diff --git a/.document b/.document
index ecf3673..6f40d63 100644
--- a/.document
+++ b/.document
@@ -1,5 +1,4 @@
-README.rdoc
+README.md
 lib/**/*.rb
 bin/*
-features/**/*.feature
 LICENSE
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..e21342e
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,13 @@
+# These are supported funding model platforms
+
+github: [pboling] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
+patreon: galtzo # Replace with a single Patreon username
+open_collective: # Replace with a single Open Collective username
+ko_fi: pboling # Replace with a single Ko-fi username
+tidelift: rubygems/oauth2 # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
+community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
+liberapay: pboling # Replace with a single Liberapay username
+issuehunt: pboling # Replace with a single IssueHunt username
+otechie: # Replace with a single Otechie username
+lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
+custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..a026766
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,8 @@
+version: 2
+updates:
+  - package-ecosystem: bundler
+    directory: "/"
+    schedule:
+      interval: daily
+      time: "04:28"
+    open-pull-requests-limit: 10
\ No newline at end of file
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
new file mode 100644
index 0000000..5832ebe
--- /dev/null
+++ b/.github/workflows/codeql-analysis.yml
@@ -0,0 +1,70 @@
+# For most projects, this workflow file will not need changing; you simply need
+# to commit it to your repository.
+#
+# You may wish to alter this file to override the set of languages analyzed,
+# or to provide custom queries or build logic.
+#
+# ******** NOTE ********
+# We have attempted to detect the languages in your repository. Please check
+# the `language` matrix defined below to confirm you have the correct set of
+# supported CodeQL languages.
+#
+name: "CodeQL"
+
+on:
+  push:
+    branches: [ master, main, "*-stable" ]
+  pull_request:
+    # The branches below must be a subset of the branches above
+    branches: [ master, main, "*-stable" ]
+  schedule:
+    - cron: '35 1 * * 5'
+
+jobs:
+  analyze:
+    name: Analyze
+    runs-on: ubuntu-latest
+    permissions:
+      actions: read
+      contents: read
+      security-events: write
+
+    strategy:
+      fail-fast: false
+      matrix:
+        language: [ 'ruby' ]
+        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
+        # Learn more about CodeQL language support at https://git.io/codeql-language-support
+
+    steps:
+    - name: Checkout repository
+      uses: actions/checkout@v2
+
+    # Initializes the CodeQL tools for scanning.
+    - name: Initialize CodeQL
+      uses: github/codeql-action/init@v1
+      with:
+        languages: ${{ matrix.language }}
+        # If you wish to specify custom queries, you can do so here or in a config file.
+        # By default, queries listed here will override any specified in a config file.
+        # Prefix the list here with "+" to use these queries and those in the config file.
+        # queries: ./path/to/local/query, your-org/your-repo/queries@main
+
+    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
+    # If this step fails, then you should remove it and run the build manually (see below)
+    - name: Autobuild
+      uses: github/codeql-action/autobuild@v1
+
+    # ℹ️ Command-line programs to run using the OS shell.
+    # 📚 https://git.io/JvXDl
+
+    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
+    #    and modify them (or add more) to build your code if your project
+    #    uses a compiled language
+
+    #- run: |
+    #   make bootstrap
+    #   make release
+
+    - name: Perform CodeQL Analysis
+      uses: github/codeql-action/analyze@v1
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
new file mode 100644
index 0000000..c3c94a5
--- /dev/null
+++ b/.github/workflows/coverage.yml
@@ -0,0 +1,113 @@
+name: Code Coverage
+
+env:
+  CI_CODECOV: true
+  COVER_ALL: true
+
+on:
+  push:
+    branches:
+      - 'main'
+      - 'master'
+      - '*-maintenance'
+      - '*-dev'
+      - '*-stable'
+    tags:
+      - '!*' # Do not execute on tags
+  pull_request:
+    branches:
+      - '*'
+  # Allow manually triggering the workflow.
+  workflow_dispatch:
+
+# Cancels all previous workflow runs for the same branch that have not yet completed.
+concurrency:
+  # The concurrency group contains the workflow name and the branch name.
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+jobs:
+  test:
+    name: Specs with Coverage - Ruby ${{ matrix.ruby }} ${{matrix.gemfile}} ${{ matrix.name_extra || '' }}
+    if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')"
+    strategy:
+      fail-fast: false
+      matrix:
+        experimental: [false]
+        rubygems:
+          - latest
+        bundler:
+          - latest
+        ruby:
+          - "2.7"
+
+    runs-on: ubuntu-latest
+    continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }}
+    steps:
+      - name: Install cURL Headers
+        run: sudo apt-get install libcurl4-openssl-dev
+
+      - uses: amancevice/setup-code-climate@v0
+        name: CodeClimate Install
+        if: matrix.ruby == '2.7' && github.event_name != 'pull_request' && always()
+        with:
+          cc_test_reporter_id: ${{ secrets.CC_TEST_REPORTER_ID }}
+
+      - name: Checkout
+        uses: actions/checkout@v2
+
+      - name: Setup Ruby & Bundle
+        uses: ruby/setup-ruby@v1
+        with:
+          ruby-version: ${{ matrix.ruby }}
+          rubygems: ${{ matrix.rubygems }}
+          bundler: ${{ matrix.bundler }}
+          bundler-cache: true
+
+      - name: CodeClimate Pre-build Notification
+        run: cc-test-reporter before-build
+        if: matrix.ruby == '2.7' && github.event_name != 'pull_request' && always()
+        continue-on-error: ${{ matrix.experimental != 'false' }}
+
+      - name: Run tests
+        run: bundle exec rake test
+
+      - name: Code Coverage Summary Report
+        uses: irongut/CodeCoverageSummary@v1.2.0
+        with:
+          filename: ./coverage/coverage.xml
+          badge: true
+          fail_below_min: true
+          format: markdown
+          hide_branch_rate: true
+          hide_complexity: true
+          indicators: true
+          output: both
+          thresholds: '95 97'
+        continue-on-error: ${{ matrix.experimental != 'false' }}
+
+      - name: Add Coverage PR Comment
+        uses: marocchino/sticky-pull-request-comment@v2
+        if: matrix.ruby == '2.7' && github.event_name == 'pull_request' && always()
+        with:
+          recreate: true
+          path: code-coverage-results.md
+        continue-on-error: ${{ matrix.experimental != 'false' }}
+
+      - name: Coveralls
+        uses: coverallsapp/github-action@master
+        if: matrix.ruby == '2.7' && github.event_name != 'pull_request' && always()
+        with:
+          github-token: ${{ secrets.GITHUB_TOKEN }}
+        continue-on-error: ${{ matrix.experimental != 'false' }}
+
+#      Using the codecov gem instead.
+#      - name: CodeCov
+#        uses: codecov/codecov-action@v2
+#        if: matrix.ruby == '2.7' && github.event_name != 'pull_request' && always()
+#        with:
+#          files: ./coverage/coverage.xml
+#          flags: unittests
+#          name: codecov-upload
+#          fail_ci_if_error: true
+#        continue-on-error: ${{ matrix.experimental != 'false' }}
diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml
new file mode 100644
index 0000000..7723f57
--- /dev/null
+++ b/.github/workflows/danger.yml
@@ -0,0 +1,47 @@
+name: What's up Danger?
+
+on:
+  pull_request:
+    branches:
+      - 'main'
+      - 'master'
+      - '*-stable'
+
+jobs:
+  danger:
+    runs-on: ubuntu-latest
+    env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps
+      BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile
+    if: false
+    # if: github.event_name  == 'pull_request' # if only run pull request when multiple trigger workflow
+    strategy:
+      fail-fast: false
+      matrix:
+        gemfile:
+          - f2
+        rubygems:
+          - latest
+        bundler:
+          - latest
+        ruby:
+          - 2.7
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+      - name: Install cURL Headers
+        run: sudo apt-get install libcurl4-openssl-dev
+      - name: Setup Ruby & Bundle
+        uses: ruby/setup-ruby@v1
+        with:
+          ruby-version: ${{ matrix.ruby }}
+          rubygems: ${{ matrix.rubygems }}
+          bundler: ${{ matrix.bundler }}
+          bundler-cache: true
+      - uses: MeilCli/danger-action@v5
+        with:
+          plugins_file: 'Gemfile'
+          install_path: 'vendor/bundle'
+          danger_file: 'Dangerfile'
+          danger_id: 'danger-pr'
+        env:
+          DANGER_GITHUB_API_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }}
diff --git a/.github/workflows/heads.yml b/.github/workflows/heads.yml
new file mode 100644
index 0000000..6dece37
--- /dev/null
+++ b/.github/workflows/heads.yml
@@ -0,0 +1,73 @@
+name: Heads
+
+on:
+  push:
+    branches:
+      - 'main'
+      - 'master'
+      - '*-maintenance'
+      - '*-dev'
+      - '*-stable'
+    tags:
+      - '!*' # Do not execute on tags
+  pull_request:
+    branches:
+      - '*'
+  # Allow manually triggering the workflow.
+  workflow_dispatch:
+
+# Cancels all previous workflow runs for the same branch that have not yet completed.
+concurrency:
+  # The concurrency group contains the workflow name and the branch name.
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+jobs:
+  test:
+    name: Specs - Ruby ${{ matrix.ruby }} ${{matrix.gemfile}} ${{ matrix.name_extra || '' }}
+    env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps
+      BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile
+    if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')"
+    strategy:
+      fail-fast: false
+      matrix:
+        experimental: [true]
+        gemfile:
+          - f0
+          - f1
+          - f2
+        rubygems:
+          - latest
+        bundler:
+          - latest
+        ruby:
+          - truffleruby+graalvm-head
+          - truffleruby-head
+          - jruby-head
+          - ruby-head
+        include:
+          # Includes a new variable experimental with a value of false
+          #   for the matrix legs matching rubygems: latest, which is all of them.
+          # This is here for parity with the unsupported.yml
+          # This is a hack. Combined with continue-on-error it should allow us
+          #   to have a workflow with allowed failure.
+          # This is the "supported" build matrix, so only the "head" builds are experimental here.
+          - rubygems: latest
+            experimental: true
+
+    runs-on: ubuntu-latest
+    continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }}
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+      - name: Install cURL Headers
+        run: sudo apt-get install libcurl4-openssl-dev
+      - name: Setup Ruby & Bundle
+        uses: ruby/setup-ruby@v1
+        with:
+          ruby-version: ${{ matrix.ruby }}
+          rubygems: ${{ matrix.rubygems }}
+          bundler: ${{ matrix.bundler }}
+          bundler-cache: true
+      - name: Run tests
+        run: bundle exec rake test
diff --git a/.github/workflows/macos-ancient.yml b/.github/workflows/macos-ancient.yml
new file mode 100644
index 0000000..ba2ebf6
--- /dev/null
+++ b/.github/workflows/macos-ancient.yml
@@ -0,0 +1,58 @@
+name: Old MacOS
+
+on:
+  push:
+    branches:
+      - 'main'
+      - 'master'
+      - '*-maintenance'
+      - '*-dev'
+      - '*-stable'
+    tags:
+      - '!*' # Do not execute on tags
+  pull_request:
+    branches:
+      - '*'
+  # Allow manually triggering the workflow.
+  workflow_dispatch:
+
+# Cancels all previous workflow runs for the same branch that have not yet completed.
+concurrency:
+  # The concurrency group contains the workflow name and the branch name.
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+jobs:
+  test:
+    name: Specs - Ruby ${{ matrix.ruby }} ${{matrix.gemfile}} ${{ matrix.name_extra || '' }}
+    env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps
+      BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile
+    if: false
+    # if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')"
+    strategy:
+      fail-fast: false
+      matrix:
+        experimental: [true]
+        gemfile:
+          - f0
+        rubygems:
+          - "2.7.11"
+        ruby:
+          - "1.9"
+          - "2.0"
+          - "2.1"
+          - "2.2"
+
+    runs-on: macos-10.15
+    continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }}
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+      - name: Setup Ruby & Bundle
+        uses: ruby/setup-ruby@v1
+        with:
+          ruby-version: ${{ matrix.ruby }}
+          rubygems: ${{ matrix.rubygems }}
+          bundler-cache: true
+      - name: Run tests
+        run: bundle exec rake test
diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml
new file mode 100644
index 0000000..24a0e8a
--- /dev/null
+++ b/.github/workflows/macos.yml
@@ -0,0 +1,61 @@
+name: MacOS
+
+on:
+  push:
+    branches:
+      - 'main'
+      - 'master'
+      - '*-maintenance'
+      - '*-dev'
+      - '*-stable'
+    tags:
+      - '!*' # Do not execute on tags
+  pull_request:
+    branches:
+      - '*'
+  # Allow manually triggering the workflow.
+  workflow_dispatch:
+
+# Cancels all previous workflow runs for the same branch that have not yet completed.
+concurrency:
+  # The concurrency group contains the workflow name and the branch name.
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+jobs:
+  test:
+    name: Specs - Ruby ${{ matrix.ruby }} ${{matrix.gemfile}} ${{ matrix.name_extra || '' }}
+    env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps
+      BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile
+    if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')"
+    strategy:
+      fail-fast: false
+      matrix:
+        experimental: [true]
+        gemfile:
+          - f2
+        rubygems:
+          - latest
+        bundler:
+          - latest
+        ruby:
+          - "2.7"
+          - "3.0"
+          - "3.1"
+          - truffleruby
+          - jruby
+
+    runs-on: macos-latest
+    continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }}
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+      - name: Setup Ruby & Bundle
+        uses: ruby/setup-ruby@v1
+        with:
+          ruby-version: ${{ matrix.ruby }}
+          rubygems: ${{ matrix.rubygems }}
+          bundler: ${{ matrix.bundler }}
+          bundler-cache: true
+      - name: Run tests
+        run: bundle exec rake test
diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml
new file mode 100644
index 0000000..22c5ac7
--- /dev/null
+++ b/.github/workflows/style.yml
@@ -0,0 +1,45 @@
+name: Code Style Checks
+
+on:
+  push:
+    branches:
+      - 'main'
+      - 'master'
+      - '*-maintenance'
+      - '*-dev'
+      - '*-stable'
+    tags:
+      - '!*' # Do not execute on tags
+  pull_request:
+    branches:
+      - '*'
+
+jobs:
+  rubocop:
+    name: Rubocop
+    if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')"
+    strategy:
+      fail-fast: false
+      matrix:
+        experimental: [false]
+        rubygems:
+          - latest
+        bundler:
+          - latest
+        ruby:
+          - 2.7
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+      - name: Install cURL Headers
+        run: sudo apt-get install libcurl4-openssl-dev
+      - name: Setup Ruby & Bundle
+        uses: ruby/setup-ruby@v1
+        with:
+          ruby-version: ${{ matrix.ruby }}
+          rubygems: ${{ matrix.rubygems }}
+          bundler: ${{ matrix.bundler }}
+          bundler-cache: true
+      - name: Run Rubocop
+        run: bundle exec rubocop -DESP
diff --git a/.github/workflows/supported.yml b/.github/workflows/supported.yml
new file mode 100644
index 0000000..b0e3b5a
--- /dev/null
+++ b/.github/workflows/supported.yml
@@ -0,0 +1,65 @@
+name: Official Support
+
+on:
+  push:
+    branches:
+      - 'main'
+      - 'master'
+      - '*-maintenance'
+      - '*-dev'
+      - '*-stable'
+    tags:
+      - '!*' # Do not execute on tags
+  pull_request:
+    branches:
+      - '*'
+  # Allow manually triggering the workflow.
+  workflow_dispatch:
+
+# Cancels all previous workflow runs for the same branch that have not yet completed.
+concurrency:
+  # The concurrency group contains the workflow name and the branch name.
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+jobs:
+  test:
+    name: Specs - Ruby ${{ matrix.ruby }} ${{matrix.gemfile}} ${{ matrix.name_extra || '' }}
+    env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps
+      BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile
+    if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')"
+    strategy:
+      fail-fast: false
+      matrix:
+        experimental: [false]
+        gemfile:
+          - f0
+          - f1
+          - f2
+        rubygems:
+          - latest
+        bundler:
+          - latest
+        ruby:
+          - "2.7"
+          - "3.0"
+          - "3.1"
+          - truffleruby
+          - jruby
+
+    runs-on: ubuntu-latest
+    continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }}
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+      - name: Install cURL Headers
+        run: sudo apt-get install libcurl4-openssl-dev
+      - name: Setup Ruby & Bundle
+        uses: ruby/setup-ruby@v1
+        with:
+          ruby-version: ${{ matrix.ruby }}
+          rubygems: ${{ matrix.rubygems }}
+          bundler: ${{ matrix.bundler }}
+          bundler-cache: true
+      - name: Run tests
+        run: bundle exec rake test
diff --git a/.github/workflows/unsupported.yml b/.github/workflows/unsupported.yml
new file mode 100644
index 0000000..3b4ee3f
--- /dev/null
+++ b/.github/workflows/unsupported.yml
@@ -0,0 +1,73 @@
+name: Unofficial Support
+
+on:
+  push:
+    branches:
+      - 'main'
+      - 'master'
+      - '*-maintenance'
+      - '*-dev'
+      - '*-stable'
+    tags:
+      - '!*' # Do not execute on tags
+  pull_request:
+    branches:
+      - '*'
+  # Allow manually triggering the workflow.
+  workflow_dispatch:
+
+# Cancels all previous workflow runs for the same branch that have not yet completed.
+concurrency:
+  # The concurrency group contains the workflow name and the branch name.
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+jobs:
+  test:
+    name: Specs - Ruby ${{ matrix.ruby }} ${{matrix.gemfile}} ${{ matrix.name_extra || '' }}
+    env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps
+      BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile
+    if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')"
+    strategy:
+      fail-fast: false
+      matrix:
+        experimental: [false]
+        gemfile:
+          - f0
+          - f1
+          - f2
+        rubygems:
+          - latest
+        bundler:
+          - latest
+        ruby:
+          - "2.3"
+          - "2.4"
+          - "2.5"
+          - "2.6"
+        exclude:
+          - ruby: "2.3"
+            gemfile: "f1"
+          - ruby: "2.3"
+            gemfile: "f2"
+          - ruby: "2.4"
+            gemfile: "f2"
+          - ruby: "2.5"
+            gemfile: "f2"
+
+    runs-on: ubuntu-20.04
+    continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }}
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+      - name: Install cURL Headers
+        run: sudo apt-get install libcurl4-openssl-dev
+      - name: Setup Ruby & Bundle
+        uses: ruby/setup-ruby@v1
+        with:
+          ruby-version: ${{ matrix.ruby }}
+          rubygems: ${{ matrix.rubygems }}
+          bundler: ${{ matrix.bundler }}
+          bundler-cache: true
+      - name: Run tests
+        run: bundle exec rake test
diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml
new file mode 100644
index 0000000..4f97a0d
--- /dev/null
+++ b/.github/workflows/windows.yml
@@ -0,0 +1,60 @@
+name: Windows
+
+on:
+  push:
+    branches:
+      - 'main'
+      - 'master'
+      - '*-maintenance'
+      - '*-dev'
+      - '*-stable'
+    tags:
+      - '!*' # Do not execute on tags
+  pull_request:
+    branches:
+      - '*'
+  # Allow manually triggering the workflow.
+  workflow_dispatch:
+
+# Cancels all previous workflow runs for the same branch that have not yet completed.
+concurrency:
+  # The concurrency group contains the workflow name and the branch name.
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
+jobs:
+  test:
+    name: Specs - Ruby ${{ matrix.ruby }} ${{matrix.gemfile}} ${{ matrix.name_extra || '' }}
+    env: # $BUNDLE_GEMFILE must be set at the job level, so it is set for all steps
+      BUNDLE_GEMFILE: ${{ github.workspace }}/gemfiles/${{ matrix.gemfile }}.gemfile
+    if: "!contains(github.event.commits[0].message, '[ci skip]') && !contains(github.event.commits[0].message, '[skip ci]')"
+    strategy:
+      fail-fast: false
+      matrix:
+        experimental: [true]
+        gemfile:
+          - f2
+        rubygems:
+          - latest
+        bundler:
+          - latest
+        ruby:
+          - "2.7"
+          - "3.0"
+          - "3.1"
+          - jruby
+
+    runs-on: windows-latest
+    continue-on-error: ${{ matrix.experimental || endsWith(matrix.ruby, 'head') }}
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+      - name: Setup Ruby & Bundle
+        uses: ruby/setup-ruby@v1
+        with:
+          ruby-version: ${{ matrix.ruby }}
+          rubygems: ${{ matrix.rubygems }}
+          bundler: ${{ matrix.bundler }}
+          bundler-cache: true
+      - name: Run tests
+        run: bundle exec rake test
diff --git a/.rspec b/.rspec
index 0912718..3629a4a 100644
--- a/.rspec
+++ b/.rspec
@@ -1,2 +1,4 @@
 --color
 --order random
+--require helper
+--format=documentation
diff --git a/.rubocop.yml b/.rubocop.yml
index c6a4197..81f22bf 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -1,30 +1,43 @@
-require: rubocop-rspec
 inherit_from:
   - .rubocop_todo.yml
   - .rubocop_rspec.yml
+
+require:
+  - 'rubocop-md'
+  - 'rubocop-packaging'
+  - 'rubocop-performance'
+  - 'rubocop-rake'
+  - 'rubocop-rspec'
+
 AllCops:
+  NewCops: enable
   DisplayCopNames: true # Display the name of the failing cops
-  TargetRubyVersion: 2.1
+  TargetRubyVersion: 2.5
   Exclude:
     - 'gemfiles/vendor/**/*'
     - 'vendor/**/*'
     - '**/.irbrc'
 
-Gemspec/RequiredRubyVersion:
-  Enabled: false
-
 Metrics/BlockLength:
+  IgnoredMethods:
+    - context
+    - describe
+    - it
+    - shared_context
+    - shared_examples
+    - shared_examples_for
+    - namespace
+    - draw
+
+Gemspec/RequiredRubyVersion:
   Enabled: false
 
 Metrics/BlockNesting:
   Max: 2
 
-Metrics/LineLength:
+Layout/LineLength:
   Enabled: false
 
-Metrics/MethodLength:
-  Max: 15
-
 Metrics/ParameterLists:
   Max: 4
 
@@ -78,3 +91,30 @@ Style/TrailingCommaInArrayLiteral:
 
 Style/TrailingCommaInHashLiteral:
   EnforcedStyleForMultiline: comma
+
+Style/HashSyntax:
+  EnforcedStyle: hash_rockets
+
+Style/Lambda:
+  Enabled: false
+
+Style/SymbolArray:
+  Enabled: false
+
+Style/EachWithObject:
+  Enabled: false
+
+# Once we drop Rubies that lack support for __dir__ we can turn this on.
+Style/ExpandPathArguments:
+  Enabled: false
+
+# On Ruby 1.9 array.to_h isn't available, needs to be Hash[array]
+Style/HashConversion:
+  Enabled: false
+
+# Turn back on once old Rubies are dropped
+Style/IfUnlessModifier:
+  Enabled: false
+
+Style/SafeNavigation:
+  Enabled: false
diff --git a/.rubocop_rspec.yml b/.rubocop_rspec.yml
index 347777d..48ad1a3 100644
--- a/.rubocop_rspec.yml
+++ b/.rubocop_rspec.yml
@@ -24,3 +24,6 @@ RSpec/NestedGroups:
   
 RSpec/ExpectInHook:
   Enabled: false
+
+RSpec/MultipleMemoizedHelpers:
+  Enabled: false
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index e770173..bae0df7 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -1,15 +1,90 @@
-Style/HashSyntax:
-  EnforcedStyle: hash_rockets
+# This configuration was generated by
+# `rubocop --auto-gen-config`
+# on 2022-02-20 04:57:56 UTC using RuboCop version 1.25.1.
+# The point is for the user to remove these configuration records
+# one by one as the offenses are removed from the code base.
+# Note that changes in the inspected code, or installation of new
+# versions of RuboCop, may require this file to be generated again.
 
-Style/Lambda:
-  Enabled: false
+# Offense count: 1
+# Configuration parameters: AllowedMethods.
+# AllowedMethods: enums
+Lint/ConstantDefinitionInBlock:
+  Exclude:
+    - 'spec/oauth2/client_spec.rb'
 
-Style/SymbolArray:
-  Enabled: false
+# Offense count: 1
+Lint/UselessAssignment:
+  Exclude:
+    - '**/*.md'
+    - '**/*.markdown'
+    - 'spec/oauth2/client_spec.rb'
 
-Style/EachWithObject:
-  Enabled: false
+# Offense count: 4
+# Configuration parameters: IgnoredMethods, CountRepeatedAttributes.
+Metrics/AbcSize:
+  Max: 43
 
-# Once we drop Rubies that lack support for __dir__ we can turn this on.
-Style/ExpandPathArguments:
-  Enabled: false
+# Offense count: 4
+# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods.
+# IgnoredMethods: refine
+Metrics/BlockLength:
+  Max: 59
+
+# Offense count: 2
+# Configuration parameters: IgnoredMethods.
+Metrics/CyclomaticComplexity:
+  Max: 11
+
+# Offense count: 8
+# Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods.
+Metrics/MethodLength:
+  Max: 37
+
+# Offense count: 1
+# Configuration parameters: IgnoredMethods.
+Metrics/PerceivedComplexity:
+  Max: 11
+
+# Offense count: 1
+# Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers.
+# SupportedStyles: snake_case, normalcase, non_integer
+# AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339
+Naming/VariableNumber:
+  Exclude:
+    - 'Gemfile'
+
+# Offense count: 2
+# Configuration parameters: MinSize.
+Performance/CollectionLiteralInLoop:
+  Exclude:
+    - 'spec/oauth2/strategy/auth_code_spec.rb'
+    - 'spec/oauth2/strategy/client_credentials_spec.rb'
+
+# Offense count: 7
+# Configuration parameters: Prefixes.
+# Prefixes: when, with, without
+RSpec/ContextWording:
+  Exclude:
+    - 'spec/oauth2/access_token_spec.rb'
+    - 'spec/oauth2/authenticator_spec.rb'
+    - 'spec/oauth2/client_spec.rb'
+
+# Offense count: 1
+RSpec/LeakyConstantDeclaration:
+  Exclude:
+    - 'spec/oauth2/client_spec.rb'
+
+# Offense count: 1
+# Cop supports --auto-correct.
+Rake/Desc:
+  Exclude:
+    - 'Rakefile'
+
+# Offense count: 3
+# Cop supports --auto-correct-all.
+# Configuration parameters: Mode.
+Style/StringConcatenation:
+  Exclude:
+    - 'lib/oauth2/authenticator.rb'
+    - 'spec/oauth2/authenticator_spec.rb'
diff --git a/.simplecov b/.simplecov
new file mode 100644
index 0000000..77f4b3f
--- /dev/null
+++ b/.simplecov
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+# To get coverage
+# On Local, default (HTML) output, it just works, coverage is turned on:
+#   bundle exec rspec spec
+# On Local, all output formats:
+#   COVER_ALL=true bundle exec rspec spec
+#
+# On CI, all output formats, the ENV variables CI is always set,
+#   and COVER_ALL, and CI_CODECOV, are set in the coverage.yml workflow only,
+#   so coverage only runs in that workflow, and outputs all formats.
+#
+if RUN_COVERAGE
+  SimpleCov.start do
+    enable_coverage :branch
+    primary_coverage :branch
+
+    if ENV['COVER_ALL']
+      require 'codecov'
+      require 'simplecov-lcov'
+      require 'simplecov-cobertura'
+
+      SimpleCov::Formatter::LcovFormatter.config do |c|
+        c.report_with_single_file = true
+        c.single_report_path = 'coverage/lcov.info'
+      end
+
+      SimpleCov.formatters = [
+        SimpleCov::Formatter::CoberturaFormatter,
+        SimpleCov::Formatter::HTMLFormatter,
+        SimpleCov::Formatter::LcovFormatter,
+        SimpleCov::Formatter::Codecov,
+      ]
+    else
+      formatter SimpleCov::Formatter::HTMLFormatter
+    end
+
+    add_filter '/spec'
+    minimum_coverage(85)
+  end
+end
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 6c65fe2..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,87 +0,0 @@
-before_install:
-  # rubygems 2.7.8 and greater include bundler
-  # - Ruby 2.2, and under, get RubyGems ~> 2.7.10, (includes bundler 1.17.3)
-  # - Anything else, including Ruby 2.3, and above, gets RubyGems ~> 3, and update bundler to latest
-  # - NOTE ON JRUBY: identifies as RUBY_VERSION ~> 1.9, 2.0, 2.3, or 2.5.
-  # - NOTE ON TRUFFLERUBY: identifies as RUBY_VERSION ~> 2.6
-  - |
-    rv="$(ruby -e 'STDOUT.write RUBY_VERSION')"
-    echo "Discovered Ruby Version of =====> $rv"
-    if   [ "$rv" \< "2.3" ]; then
-      gem update --system 2.7.10
-    elif   [ "$rv" \< "2.4" ]; then
-      gem update --system 2.7.10 --no-document
-    elif [ "$rv" = "2.5.3" ]; then
-      # JRUBY 9.2 Identifies as 2.5.3, and it fails to update rubygems
-      gem install --no-document bundler "bundler:>=2.0"
-    else
-      gem update --system --no-document --conservative
-      gem install --no-document bundler "bundler:>=2.0"
-    fi
-
-before_script:
-  - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
-  - chmod +x ./cc-test-reporter
-  - ./cc-test-reporter before-build
-
-after_script:
-  - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
-
-bundler_args: --no-deployment --jobs 3 --retry 3
-
-cache: bundler
-
-env:
-  global:
-    - JRUBY_OPTS="$JRUBY_OPTS -Xcli.debug=true --debug"
-    - CC_TEST_REPORTER_ID=29caf9cf27d27ae609c088feb9d4ba34460f7a39251f2e8615c9a16f3075530e
-
-language: ruby
-
-matrix:
-  allow_failures:
-    - rvm: jruby-head
-    - rvm: ruby-head
-    - rvm: truffleruby
-    - rvm: jruby-9.0
-    - rvm: jruby-9.1 # jruby-9.1 often fails to download, thus failing the build.
-    - rvm: jruby-9.2 # jruby-9.2 often fails to download, thus failing the build.
-  fast_finish: true
-  include:
-#    - rvm: jruby-1.7 # targets MRI v1.9
-#      gemfile: gemfiles/jruby_1.7.gemfile
-    - rvm: 1.9
-      gemfile: gemfiles/ruby_1.9.gemfile
-    - rvm: 2.0
-      gemfile: gemfiles/ruby_2.0.gemfile
-    - rvm: jruby-9.0 # targets MRI v2.0
-      gemfile: gemfiles/jruby_9.0.gemfile
-    - rvm: 2.1
-      gemfile: gemfiles/ruby_2.1.gemfile
-    # DEPRECATION WARNING
-    # oauth2 1.x series releases are the last to support Ruby versions above
-    # oauth2 2.x series releases will support Ruby versions below, and not above
-    - rvm: jruby-9.1 # targets MRI v2.3
-      gemfile: gemfiles/jruby_9.1.gemfile
-    - rvm: 2.2
-      gemfile: gemfiles/ruby_2.2.gemfile
-    - rvm: 2.3
-      gemfile: gemfiles/ruby_2.3.gemfile
-    - rvm: 2.4
-      gemfile: gemfiles/ruby_2.4.gemfile
-    - rvm: jruby-9.2 # targets MRI v2.5
-      gemfile: gemfiles/jruby_9.2.gemfile
-    - rvm: 2.5
-      gemfile: gemfiles/ruby_2.5.gemfile
-    - rvm: 2.6
-      gemfile: gemfiles/ruby_2.6.gemfile
-    - rvm: 2.7
-      gemfile: gemfiles/ruby_2.7.gemfile
-    - rvm: jruby-head
-      gemfile: gemfiles/jruby_head.gemfile
-    - rvm: ruby-head
-      gemfile: gemfiles/ruby_head.gemfile
-    - rvm: truffleruby
-      gemfile: gemfiles/truffleruby.gemfile
-
-sudo: false
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e830312..c24d4d6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,9 +1,43 @@
 # Change Log
 All notable changes to this project will be documented in this file.
 
-## [unreleased]
+## unreleased
 
-- no changes yet
+## [1.4.9] - 2022-02-20
+
+- Fixes compatibility with Faraday v2 [572](https://github.com/oauth-xx/oauth2/issues/572)
+- Includes supported versions of Faraday in test matrix:
+  - Faraday ~> 2.2.0 with Ruby >= 2.6
+  - Faraday ~> 1.10 with Ruby >= 2.4
+  - Faraday ~> 0.17.3 with Ruby >= 1.9
+- Add Windows and MacOS to test matrix
+
+## [1.4.8] - 2022-02-18
+
+- MFA is now required to push new gem versions (@pboling)
+- README overhaul w/ new Ruby Verion and Engine compatibility policies (@pboling)
+- [#569](https://github.com/oauth-xx/oauth2/pull/569) Backport fixes ([#561](https://github.com/oauth-xx/oauth2/pull/561) by @ryogift), and add more fixes, to allow faraday 1.x and 2.x (@jrochkind)
+- Improve Code Coverage tracking (Coveralls, CodeCov, CodeClimate), and enable branch coverage (@pboling)
+- Add CodeQL, Security Policy, Funding info (@pboling)
+- Added Ruby 3.1, jruby, jruby-head, truffleruby, truffleruby-head to build matrix (@pboling)
+- [#543](https://github.com/oauth-xx/oauth2/pull/543) - Support for more modern Open SSL libraries (@pboling)
+
+## [1.4.7] - 2021-03-19
+
+- [#541](https://github.com/oauth-xx/oauth2/pull/541) - Backport fix to expires_at handling [#533](https://github.com/oauth-xx/oauth2/pull/533) to 1-4-stable branch. (@dobon)
+
+## [1.4.6] - 2021-03-19
+
+- [#540](https://github.com/oauth-xx/oauth2/pull/540) - Add VERSION constant (@pboling)
+- [#537](https://github.com/oauth-xx/oauth2/pull/537) - Fix crash in OAuth2::Client#get_token (@anderscarling)
+- [#538](https://github.com/oauth-xx/oauth2/pull/538) - Remove reliance on globally included OAuth2 in tests for version 1.4 (@anderscarling)
+
+## [1.4.5] - 2021-03-18
+
+- [#535](https://github.com/oauth-xx/oauth2/pull/535) - Compatibility with range of supported Ruby OpenSSL versions, Rubocop updates, Github Actions (@pboling)
+- [#518](https://github.com/oauth-xx/oauth2/pull/518) - Add extract_access_token option to OAuth2::Client (@jonspalmer)
+- [#507](https://github.com/oauth-xx/oauth2/pull/507) - Fix camel case content type, response keys (@anvox)
+- [#500](https://github.com/oauth-xx/oauth2/pull/500) - Fix YARD documentation formatting (@olleolleolle)
 
 ## [1.4.4] - 2020-02-12
 
@@ -154,4 +188,9 @@ All notable changes to this project will be documented in this file.
 [1.4.1]: https://github.com/oauth-xx/oauth2/compare/v1.4.0...v1.4.1
 [1.4.2]: https://github.com/oauth-xx/oauth2/compare/v1.4.1...v1.4.2
 [1.4.3]: https://github.com/oauth-xx/oauth2/compare/v1.4.2...v1.4.3
+[1.4.4]: https://github.com/oauth-xx/oauth2/compare/v1.4.3...v1.4.4
+[1.4.5]: https://github.com/oauth-xx/oauth2/compare/v1.4.4...v1.4.5
+[1.4.6]: https://github.com/oauth-xx/oauth2/compare/v1.4.5...v1.4.6
+[1.4.7]: https://github.com/oauth-xx/oauth2/compare/v1.4.6...v1.4.7
+[1.4.8]: https://github.com/oauth-xx/oauth2/compare/v1.4.7...v1.4.8
 [unreleased]: https://github.com/oauth-xx/oauth2/compare/v1.4.1...HEAD
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index 395b407..99ab478 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -1,74 +1,133 @@
+
 # Contributor Covenant Code of Conduct
 
 ## Our Pledge
 
-In the interest of fostering an open and welcoming environment, we as
-contributors and maintainers pledge to making participation in our project and
-our community a harassment-free experience for everyone, regardless of age, body
-size, disability, ethnicity, gender identity and expression, level of experience,
-nationality, personal appearance, race, religion, or sexual identity and
-orientation.
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, religion, or sexual identity
+and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
 
 ## Our Standards
 
-Examples of behavior that contributes to creating a positive environment
-include:
+Examples of behavior that contributes to a positive environment for our
+community include:
 
-* Using welcoming and inclusive language
-* Being respectful of differing viewpoints and experiences
-* Gracefully accepting constructive criticism
-* Focusing on what is best for the community
-* Showing empathy towards other community members
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes,
+  and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the
+  overall community
 
-Examples of unacceptable behavior by participants include:
+Examples of unacceptable behavior include:
 
-* The use of sexualized language or imagery and unwelcome sexual attention or
-advances
-* Trolling, insulting/derogatory comments, and personal or political attacks
+* The use of sexualized language or imagery, and sexual attention or
+  advances of any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
 * Public or private harassment
-* Publishing others' private information, such as a physical or electronic
-  address, without explicit permission
+* Publishing others' private information, such as a physical or email
+  address, without their explicit permission
 * Other conduct which could reasonably be considered inappropriate in a
   professional setting
 
-## Our Responsibilities
+## Enforcement Responsibilities
 
-Project maintainers are responsible for clarifying the standards of acceptable
-behavior and are expected to take appropriate and fair corrective action in
-response to any instances of unacceptable behavior.
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
 
-Project maintainers have the right and responsibility to remove, edit, or
-reject comments, commits, code, wiki edits, issues, and other contributions
-that are not aligned to this Code of Conduct, or to ban temporarily or
-permanently any contributor for other behaviors that they deem inappropriate,
-threatening, offensive, or harmful.
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
 
 ## Scope
 
-This Code of Conduct applies both within project spaces and in public spaces
-when an individual is representing the project or its community. Examples of
-representing a project or community include using an official project e-mail
-address, posting via an official social media account, or acting as an appointed
-representative at an online or offline event. Representation of a project may be
-further defined and clarified by project maintainers.
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
 
 ## Enforcement
 
 Instances of abusive, harassing, or otherwise unacceptable behavior may be
-reported by contacting the project team at peter.boling@gmail.com. All
-complaints will be reviewed and investigated and will result in a response that
-is deemed necessary and appropriate to the circumstances. The project team is
-obligated to maintain confidentiality with regard to the reporter of an incident.
-Further details of specific enforcement policies may be posted separately.
+reported to the community leaders responsible for enforcement at
+[INSERT CONTACT METHOD].
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
 
-Project maintainers who do not follow or enforce the Code of Conduct in good
-faith may face temporary or permanent repercussions as determined by other
-members of the project's leadership.
+**Community Impact**: A violation through a single incident or series
+of actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or
+permanent ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior,  harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within
+the community.
 
 ## Attribution
 
-This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
-available at [http://contributor-covenant.org/version/1/4][version]
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.0, available at
+[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
+
+Community Impact Guidelines were inspired by
+[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
+
+For answers to common questions about this code of conduct, see the FAQ at
+[https://www.contributor-covenant.org/faq][FAQ]. Translations are available
+at [https://www.contributor-covenant.org/translations][translations].
 
-[homepage]: http://contributor-covenant.org
-[version]: http://contributor-covenant.org/version/1/4/
+[homepage]: https://www.contributor-covenant.org
+[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
+[Mozilla CoC]: https://github.com/mozilla/diversity
+[FAQ]: https://www.contributor-covenant.org/faq
+[translations]: https://www.contributor-covenant.org/translations
diff --git a/Dangerfile b/Dangerfile
new file mode 100644
index 0000000..518ea63
--- /dev/null
+++ b/Dangerfile
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+# Ideas...
+# 1. Check for hashtags in PR title, and disallow changes to changelog?
+#   e.g. github.pr_title.include? "#trivial"
+
+# Make it more obvious that a PR is a work in progress and shouldn't be merged yet
+warn('PR is classed as Work in Progress') if github.pr_title.include? '[WIP]'
+
+# Warn when there is a big PR
+warn('Big PR') if git.lines_of_code > 500
+
+# Don't let testing shortcuts get into master by accident
+raise('fdescribe left in tests') if `grep -r fdescribe specs/ `.length > 1
+raise('fit left in tests') if `grep -r fit specs/ `.length > 1
diff --git a/Gemfile b/Gemfile
index 0dbce15..e4884b0 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,40 +1,56 @@
+# frozen_string_literal: true
+
 source 'https://rubygems.org'
 
+gemspec
+
 git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
 
-gem 'faraday', ['>= 0.8', '< 2.0'], :platforms => [:jruby_18, :ruby_18]
-gem 'jwt', '< 1.5.2', :platforms => [:jruby_18, :ruby_18]
-gem 'rake', '< 11.0'
-gem 'rdoc', '~> 4.2.2'
+gem 'jwt'
+gem 'overcommit'
+gem 'rake'
 
 group :test do
-  ruby_version = Gem::Version.new(RUBY_VERSION)
-  if ruby_version >= Gem::Version.new('2.1')
-    # TODO: Upgrade to >= 0.59 when we drop Rubies below 2.2
-    #     Error: Unsupported Ruby version 2.1 found in `TargetRubyVersion` parameter (in .rubocop.yml). 2.1-compatible analysis was dropped after version 0.58.
-    #     Supported versions: 2.2, 2.3, 2.4, 2.5
-    gem 'rubocop', '~> 0.57.0'
-    gem 'rubocop-rspec', '~> 1.27.0' # last version that can use rubocop < 0.58
-  end
-  gem 'pry', '~> 0.11' if ruby_version >= Gem::Version.new('2.0')
-
   gem 'addressable', '~> 2.3.8'
   gem 'backports'
-  gem 'coveralls'
-  gem 'rack', '~> 1.2', :platforms => [:jruby_18, :jruby_19, :ruby_18, :ruby_19, :ruby_20, :ruby_21]
+  gem 'rack', '~> 1.2', :platforms => [:ruby_21]
   gem 'rspec', '>= 3'
-  gem 'simplecov', '>= 0.9'
-
-  platforms :jruby_18, :ruby_18 do
-    gem 'mime-types', '~> 1.25'
-    gem 'rest-client', '~> 1.6.0'
-  end
-
-  platforms :ruby_18, :ruby_19 do
-    gem 'json', '< 2.0'
-    gem 'term-ansicolor', '< 1.4.0'
-    gem 'tins', '< 1.7'
+  platforms :mri do
+    ruby_version = Gem::Version.new(RUBY_VERSION)
+    minimum_version = ->(version) { ruby_version >= Gem::Version.new(version) && RUBY_ENGINE == 'ruby' }
+    linting = minimum_version.call('2.7')
+    coverage = minimum_version.call('2.7')
+    debug = minimum_version.call('2.5')
+    if linting
+      # Danger is incompatible with Faraday 2 (for now)
+      # see: https://github.com/danger/danger/issues/1349
+      # gem 'danger', '~> 8.4'
+      gem 'rubocop', '~> 1.22', :require => false
+      gem 'rubocop-md', '~> 1.0', :require => false
+      gem 'rubocop-packaging', '~> 0.5', :require => false
+      gem 'rubocop-performance', '~> 1.11', :require => false
+      gem 'rubocop-rake', '~> 0.6', :require => false
+      gem 'rubocop-rspec', :require => false
+      gem 'rubocop-thread_safety', '~> 0.4', :require => false
+    end
+    if coverage
+      gem 'codecov', :require => false, :group => :test
+      gem 'simplecov', '~> 0.21', :require => false
+      gem 'simplecov-cobertura' # XML for Jenkins
+      gem 'simplecov-lcov', '~> 0.8', :require => false
+    end
+    if debug
+      # No need to run byebug / pry on earlier versions
+      gem 'byebug'
+      gem 'pry'
+      gem 'pry-byebug'
+    end
   end
 end
 
-gemspec
+### deps for documentation and rdoc.info
+group :documentation do
+  gem 'github-markup', :platform => :mri
+  gem 'redcarpet', :platform => :mri
+  gem 'yard', :require => false
+end
diff --git a/LICENSE b/LICENSE
index 6dda5ca..0d3a82c 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,7 +1,7 @@
 MIT License
 
 Copyright (c) 2011 - 2013 Michael Bleigh and Intridea, Inc.
-Copyright (c) 2017 - 2018 oauth-xx organization, https://github.com/oauth-xx
+Copyright (c) 2017 - 2022 oauth-xx organization, https://github.com/oauth-xx
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 55ea4d8..f5e8349 100644
--- a/README.md
+++ b/README.md
@@ -1,73 +1,260 @@
-# OAuth2
+<p align="center">
+    <a href="http://oauth.net/2/" target="_blank" rel="noopener noreferrer">
+      <img src="https://github.com/oauth-xx/oauth2/raw/master/docs/images/logo/oauth2-logo-124px.png?raw=true" alt="OAuth 2.0 Logo by Chris Messina, CC BY-SA 3.0">
+    </a>
+    <a href="https://www.ruby-lang.org/" target="_blank" rel="noopener noreferrer">
+      <img width="124px" src="https://github.com/oauth-xx/oauth2/raw/master/docs/images/logo/ruby-logo-198px.svg?raw=true" alt="Yukihiro Matsumoto, Ruby Visual Identity Team, CC BY-SA 2.5">
+    </a>
+</p>
+
+## What
+
+OAuth 2.0 is the industry-standard protocol for authorization.
+OAuth 2.0 focuses on client developer simplicity while providing specific authorization flows for web applications,
+    desktop applications, mobile phones, and living room devices.
+This is a RubyGem for implementing OAuth 2.0 clients and servers in Ruby applications.
+See the sibling `oauth` gem for OAuth 1.0 implementations in Ruby.
+
+⚠️ **_WARNING_**: You are viewing the `README` of the soon-to-be-deprecated `1-4-stable`
+branch which for version 1.4.x releases. Version 2.0 is coming! ⚠️
 
-If you need the readme for a released version of the gem please find it below:
+---
+
+* [OAuth 2.0 Spec][oauth2-spec]
+* [OAuth 1.0 sibling gem][sibling-gem]
+* Help us finish release [![2.0.0 release milestone][next-milestone-pct-img]][next-milestone-pct] by submitting or reviewing PRs and issues.
+* Oauth2 gem is _always_ looking for additional maintainers. See [#307][maintainers-discussion].
+
+[oauth2-spec]: https://oauth.net/2/
+[sibling-gem]: https://github.com/oauth-xx/oauth-ruby
+[next-milestone-pct]: https://github.com/oauth-xx/oauth2/milestone/1
+[next-milestone-pct-img]: https://img.shields.io/github/milestones/progress-percent/oauth-xx/oauth2/1
+[maintainers-discussion]: https://github.com/oauth-xx/oauth2/issues/307
+
+## Release Documentation
+
+<details>
+  <summary>1.4.x Readmes</summary>
+
+| Version | Release Date | Readme                                                   |
+|---------|--------------|----------------------------------------------------------|
+| 1.4.8   | Feb 18, 2022 | https://github.com/oauth-xx/oauth2/blob/v1.4.8/README.md |
+| 1.4.7   | Mar 19, 2021 | https://github.com/oauth-xx/oauth2/blob/v1.4.7/README.md |
+| 1.4.6   | Mar 19, 2021 | https://github.com/oauth-xx/oauth2/blob/v1.4.6/README.md |
+| 1.4.5   | Mar 18, 2021 | https://github.com/oauth-xx/oauth2/blob/v1.4.5/README.md |
+| 1.4.4   | Feb 12, 2020 | https://github.com/oauth-xx/oauth2/blob/v1.4.4/README.md |
+| 1.4.3   | Jan 29, 2020 | https://github.com/oauth-xx/oauth2/blob/v1.4.3/README.md |
+| 1.4.2   | Oct 1, 2019  | https://github.com/oauth-xx/oauth2/blob/v1.4.2/README.md |
+| 1.4.1   | Oct 13, 2018 | https://github.com/oauth-xx/oauth2/blob/v1.4.1/README.md |
+| 1.4.0   | Jun 9, 2017  | https://github.com/oauth-xx/oauth2/blob/v1.4.0/README.md |
+</details>
+
+<details>
+  <summary>1.3.x Readmes</summary>
 
 | Version  | Release Date | Readme                                                   |
 |----------|--------------|----------------------------------------------------------|
-| 1.4.4    | Feb 12, 2020 | https://github.com/oauth-xx/oauth2/blob/v1.4.4/README.md |
-| 1.4.3    | Jan 29, 2020 | https://github.com/oauth-xx/oauth2/blob/v1.4.3/README.md |
-| 1.4.2    | Oct 1, 2019  | https://github.com/oauth-xx/oauth2/blob/v1.4.2/README.md |
-| 1.4.1    | Oct 13, 2018 | https://github.com/oauth-xx/oauth2/blob/v1.4.1/README.md |
-| 1.4.0    | Jun 9, 2017  | https://github.com/oauth-xx/oauth2/blob/v1.4.0/README.md |
 | 1.3.1    | Mar 3, 2017  | https://github.com/oauth-xx/oauth2/blob/v1.3.1/README.md |
 | 1.3.0    | Dec 27, 2016 | https://github.com/oauth-xx/oauth2/blob/v1.3.0/README.md |
+</details>
+
+<details>
+  <summary>&le;= 1.2.x Readmes (2016 and before)</summary>
+
+| Version  | Release Date | Readme                                                   |
+|----------|--------------|----------------------------------------------------------|
 | 1.2.0    | Jun 30, 2016 | https://github.com/oauth-xx/oauth2/blob/v1.2.0/README.md |
 | 1.1.0    | Jan 30, 2016 | https://github.com/oauth-xx/oauth2/blob/v1.1.0/README.md |
 | 1.0.0    | May 23, 2014 | https://github.com/oauth-xx/oauth2/blob/v1.0.0/README.md |
 | < 1.0.0  | Find here    | https://github.com/oauth-xx/oauth2/tags                  |
+</details>
+
+<!--
+Numbering rows and badges in each row as a visual "database" lookup,
+    as the table is extremely dense, and it can be very difficult to find anything
+Putting one on each row here, to document the emoji that should be used, and for ease of copy/paste.
+
+row #s:
+1️⃣
+2️⃣
+3️⃣
+4️⃣
+5️⃣
+6️⃣
+7️⃣
+
+badge #s:
+⛳️
+🖇
+🏘
+🚎
+🖐
+🧮
+📗
+-->
+
+|     | Project               | oauth2                                                                                                                                                                                                                                                              |
+|:----|-----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| 1️⃣ | name, license, docs   | [![RubyGems.org][⛳️name-img]][⛳️gem] [![License: MIT][🖇src-license-img]][🖇src-license] [![FOSSA][🏘fossa-img]][🏘fossa] [![RubyDoc.info][🚎yard-img]][🚎yard] [![InchCI][🖐inch-ci-img]][🚎yard]                                                                  |
+| 2️⃣ | version & activity    | [![Gem Version][⛳️version-img]][⛳️gem] [![Total Downloads][🖇DL-total-img]][⛳️gem] [![Download Rank][🏘DL-rank-img]][⛳️gem] [![Source Code][🚎src-home-img]][🚎src-home] [![Open PRs][🖐prs-open-img]][🖐prs-open] [![Closed PRs][🧮prs-closed-img]][🧮prs-closed]  |
+| 3️⃣ | maintanence & linting | [![Maintainability][⛳cclim-maint-img]][⛳cclim-maint] [![Helpers][🖇triage-help-img]][🖇triage-help] [![Depfu][🏘depfu-img]][🏘depfu] [![Contributors][🚎contributors-img]][🚎contributors] [![Style][🖐style-wf-img]][🖐style-wf] [![Kloc Roll][🧮kloc-img]][🧮kloc] |
+| 4️⃣ | testing               | [![Build][⛳️tot-bld-img]][⛳️tot-bld] [![supported][🖇supported-wf-img]][🖇supported-wf] [![EOL & Code Coverage Build][🏘eol-wf-img]][🏘eol-wf] [![unsupported][🚎unsupported-wf-img]][🚎unsupported-wf]                                                             |
+| 5️⃣ | coverage & security   | [![CodeClimate][⛳cclim-cov-img]][⛳cclim-cov] [![CodeCov][🖇codecov-img]][🖇codecov] [![Coveralls][🏘coveralls-img]][🏘coveralls] [![Security Policy][🚎sec-pol-img]][🚎sec-pol] [![CodeQL][🖐codeQL-img]][🖐codeQL]                                                 |
+| 6️⃣ | resources             | [![Discussion][⛳gh-discussions-img]][⛳gh-discussions] [![Get help on Codementor][🖇codementor-img]][🖇codementor] [![Chat][🏘chat-img]][🏘chat] [![Blog][🚎blog-img]][🚎blog] [![Blog][🖐wiki-img]][🖐wiki]                                                         |
+| 7️⃣ | spread 💖             | [![Liberapay Patrons][⛳liberapay-img]][⛳liberapay] [![Sponsor Me][🖇sponsor-img]][🖇sponsor] [![Tweet @ Peter][🏘tweet-img]][🏘tweet] [🌏][aboutme] [👼][angelme] [💻][coderme] [🌹][politicme]                                                                     |
+
+<!--
+The link tokens in the following sections should be kept ordered by the row and badge numbering scheme
+-->
+
+<!-- 1️⃣ name, license, docs -->
+[⛳️gem]: https://rubygems.org/gems/oauth2
+[⛳️name-img]: https://img.shields.io/badge/name-oauth2-brightgreen.svg?style=flat
+[🖇src-license]: https://opensource.org/licenses/MIT
+[🖇src-license-img]: https://img.shields.io/badge/License-MIT-green.svg
+[🏘fossa]: https://app.fossa.io/projects/git%2Bgithub.com%2Foauth-xx%2Foauth2?ref=badge_shield
+[🏘fossa-img]: https://app.fossa.io/api/projects/git%2Bgithub.com%2Foauth-xx%2Foauth2.svg?type=shield
+[🚎yard]: https://www.rubydoc.info/github/oauth-xx/oauth2
+[🚎yard-img]: https://img.shields.io/badge/documentation-rubydoc-brightgreen.svg?style=flat
+[🖐inch-ci-img]: http://inch-ci.org/github/oauth-xx/oauth2.png
+
+<!-- 2️⃣ version & activity -->
+[⛳️version-img]: http://img.shields.io/gem/v/oauth2.svg
+[🖇DL-total-img]: https://img.shields.io/gem/dt/oauth2.svg
+[🏘DL-rank-img]: https://img.shields.io/gem/rt/oauth2.svg
+[🚎src-home]: https://github.com/oauth-xx/oauth2
+[🚎src-home-img]: https://img.shields.io/badge/source-github-brightgreen.svg?style=flat
+[🖐prs-open]: https://github.com/oauth-xx/oauth2/pulls
+[🖐prs-open-img]: https://img.shields.io/github/issues-pr/oauth-xx/oauth2
+[🧮prs-closed]: https://github.com/oauth-xx/oauth2/pulls?q=is%3Apr+is%3Aclosed
+[🧮prs-closed-img]: https://img.shields.io/github/issues-pr-closed/oauth-xx/oauth2
+
+<!-- 3️⃣ maintanence & linting -->
+[⛳cclim-maint]: https://codeclimate.com/github/oauth-xx/oauth2/maintainability
+[⛳cclim-maint-img]: https://api.codeclimate.com/v1/badges/688c612528ff90a46955/maintainability
+[🖇triage-help]: https://www.codetriage.com/oauth-xx/oauth2
+[🖇triage-help-img]: https://www.codetriage.com/oauth-xx/oauth2/badges/users.svg
+[🏘depfu]: https://depfu.com/github/oauth-xx/oauth2?project_id=4445
+[🏘depfu-img]: https://badges.depfu.com/badges/6d34dc1ba682bbdf9ae2a97848241743/count.svg
+[🚎contributors]: https://github.com/oauth-xx/oauth2/graphs/contributors
+[🚎contributors-img]: https://img.shields.io/github/contributors-anon/oauth-xx/oauth2
+[🖐style-wf]: https://github.com/oauth-xx/oauth2/actions/workflows/style.yml
+[🖐style-wf-img]: https://github.com/oauth-xx/oauth2/actions/workflows/style.yml/badge.svg
+[🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
+[🧮kloc-img]: https://img.shields.io/tokei/lines/github.com/oauth-xx/oauth2
+
+<!-- 4️⃣ testing -->
+[⛳️tot-bld]: https://actions-badge.atrox.dev/oauth-xx/oauth2/goto
+[⛳️tot-bld-img]: https://img.shields.io/endpoint.svg?url=https%3A%2F%2Factions-badge.atrox.dev%2Foauth-xx%2Foauth2%2Fbadge&style=flat
+[🖇supported-wf]: https://github.com/oauth-xx/oauth2/actions/workflows/supported.yml
+[🖇supported-wf-img]: https://github.com/oauth-xx/oauth2/actions/workflows/supported.yml/badge.svg
+[🏘eol-wf]: https://github.com/oauth-xx/oauth2/actions/workflows/coverage.yml
+[🏘eol-wf-img]: https://github.com/oauth-xx/oauth2/actions/workflows/coverage.yml/badge.svg
+[🚎unsupported-wf]: https://github.com/oauth-xx/oauth2/actions/workflows/unsupported.yml
+[🚎unsupported-wf-img]: https://github.com/oauth-xx/oauth2/actions/workflows/unsupported.yml/badge.svg
+[🖐issues]: https://github.com/oauth-xx/oauth2/issues
+[🖐issues-img]: https://github.com/oauth-xx/oauth2/issues
+
+<!-- 5️⃣ coverage & security -->
+[⛳cclim-cov]: https://codeclimate.com/github/oauth-xx/oauth2/test_coverage
+[⛳cclim-cov-img]: https://api.codeclimate.com/v1/badges/688c612528ff90a46955/test_coverage
+[🖇codecov-img]: https://codecov.io/gh/oauth-xx/oauth2/branch/1-4-stable/graph/badge.svg?token=bNqSzNiuo2
+[🖇codecov]: https://codecov.io/gh/oauth-xx/oauth2
+[🏘coveralls]: https://coveralls.io/github/oauth-xx/oauth2?branch=1-4-stable
+[🏘coveralls-img]: https://coveralls.io/repos/github/oauth-xx/oauth2/badge.svg?branch=1-4-stable
+[🚎sec-pol]: https://github.com/oauth-xx/oauth2/blob/master/SECURITY.md
+[🚎sec-pol-img]: https://img.shields.io/badge/security-policy-brightgreen.svg?style=flat
+[🖐codeQL]: https://github.com/oauth-xx/oauth2/security/code-scanning
+[🖐codeQL-img]: https://github.com/oauth-xx/oauth2/actions/workflows/codeql-analysis.yml/badge.svg
+
+<!-- 6️⃣ resources -->
+[⛳gh-discussions]: https://github.com/oauth-xx/oauth2/discussions
+[⛳gh-discussions-img]: https://img.shields.io/github/discussions/oauth-xx/oauth2
+[🖇codementor]: https://www.codementor.io/peterboling?utm_source=github&utm_medium=button&utm_term=peterboling&utm_campaign=github
+[🖇codementor-img]: https://cdn.codementor.io/badges/get_help_github.svg
+[🏘chat]: https://gitter.im/oauth-xx/oauth2
+[🏘chat-img]: https://img.shields.io/gitter/room/oauth-xx/oauth2.svg
+[🚎blog]: http://www.railsbling.com/tags/oauth2/
+[🚎blog-img]: https://img.shields.io/badge/blog-railsbling-brightgreen.svg?style=flat
+[🖐wiki]: https://github.com/oauth-xx/oauth2/wiki
+[🖐wiki-img]: https://img.shields.io/badge/wiki-examples-brightgreen.svg?style=flat
+
+<!-- 7️⃣ spread 💖 -->
+[⛳liberapay-img]: https://img.shields.io/liberapay/patrons/pboling.svg?logo=liberapay
+[⛳liberapay]: https://liberapay.com/pboling/donate
+[🖇sponsor-img]: https://img.shields.io/badge/sponsor-pboling.svg?style=social&logo=github
+[🖇sponsor]: https://github.com/sponsors/pboling
+[🏘tweet-img]: https://img.shields.io/twitter/follow/galtzo.svg?style=social&label=Follow
+[🏘tweet]: http://twitter.com/galtzo
+
+<!-- Maintainer Contact Links -->
+[railsbling]: http://www.railsbling.com
+[peterboling]: http://www.peterboling.com
+[aboutme]: https://about.me/peter.boling
+[angelme]: https://angel.co/peter-boling
+[coderme]:http://coderwall.com/pboling
+[politicme]: https://nationalprogressiveparty.org
 
-[![Gem Version](http://img.shields.io/gem/v/oauth2.svg)][gem]
-[![Total Downloads](https://img.shields.io/gem/dt/oauth2.svg)][gem]
-[![Downloads Today](https://img.shields.io/gem/rt/oauth2.svg)][gem]
-[![Build Status](https://travis-ci.org/oauth-xx/oauth2.svg?branch=1-4-stable)][travis]
-[![Test Coverage](https://api.codeclimate.com/v1/badges/688c612528ff90a46955/test_coverage)][codeclimate-coverage]
-[![Maintainability](https://api.codeclimate.com/v1/badges/688c612528ff90a46955/maintainability)][codeclimate-maintainability]
-[![Depfu](https://badges.depfu.com/badges/6d34dc1ba682bbdf9ae2a97848241743/count.svg)][depfu]
-[![Open Source Helpers](https://www.codetriage.com/oauth-xx/oauth2/badges/users.svg)][code-triage]
-[![Chat](https://img.shields.io/gitter/room/oauth-xx/oauth2.svg)](https://gitter.im/oauth-xx/oauth2)
-[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)][source-license]
-[![Documentation](http://inch-ci.org/github/oauth-xx/oauth2.png)][inch-ci]
-
-[gem]: https://rubygems.org/gems/oauth2
-[travis]: http://travis-ci.org/oauth-xx/oauth2
-[coveralls]: https://coveralls.io/r/oauth-xx/oauth2
-[codeclimate-maintainability]: https://codeclimate.com/github/oauth-xx/oauth2/maintainability
-[codeclimate-coverage]: https://codeclimate.com/github/oauth-xx/oauth2/test_coverage
-[depfu]: https://depfu.com/github/oauth-xx/oauth2
-[source-license]: https://opensource.org/licenses/MIT
-[inch-ci]: http://inch-ci.org/github/oauth-xx/oauth2
-[code-triage]: https://www.codetriage.com/oauth-xx/oauth2
-[fossa1]: https://app.fossa.io/projects/git%2Bgithub.com%2Foauth-xx%2Foauth2?ref=badge_shield
-
-A Ruby wrapper for the [OAuth 2.0 specification][oauth2-spec].
-
-[oauth2-spec]: https://oauth.net/2/
 
 ## Installation
 
-Add this line to your application's Gemfile:
+```shell
+gem install oauth2
+```
+
+Or inside a `Gemfile`
 
 ```ruby
 gem 'oauth2'
 ```
+And then execute in a shell:
+```shell
+bundle
+```
 
-And then execute:
 
-    $ bundle
+## Compatibility
 
-Or install it yourself as:
+Targeted ruby compatibility is non-EOL versions of Ruby, currently 2.7, 3.0 and
+3.1. Compatibility is further distinguished by supported and unsupported versions of Ruby.
+Ruby is limited to 1.9+ in the gemspec for the 1.4.x series and will be 2.2+ for 2.x releases (see `master` branch).
 
-    $ gem install oauth2
+<details>
+  <summary>Ruby Engine Compatibility Policy</summary>
 
-## Resources
+This gem is tested against MRI, JRuby, and Truffleruby.
+Each of those has varying versions that target a specific version of MRI Ruby.
+This gem should work in the just-listed Ruby engines according to the targeted MRI compatibility in the table below.
+If you would like to add support for additional engines,
+  first make sure Github Actions supports the engine,
+  then submit a PR to the correct maintenance branch as according to the table below.
+</details>
 
-* [View Source on GitHub][code]
-* [Report Issues on GitHub][issues]
-* [Read More at the Wiki][wiki]
+<details>
+  <summary>Ruby Version Compatibility Policy</summary>
 
-[code]: https://github.com/oauth-xx/oauth2
-[issues]: https://github.com/oauth-xx/oauth2/issues
-[wiki]: https://wiki.github.com/oauth-xx/oauth2
+If something doesn't work on one of these interpreters, it's a bug.
+
+This library may inadvertently work (or seem to work) on other Ruby
+implementations, however support will only be provided for the versions listed
+above.
+
+If you would like this library to support another Ruby version, you may
+volunteer to be a maintainer. Being a maintainer entails making sure all tests
+run and pass on that implementation. When something breaks on your
+implementation, you will be responsible for providing patches in a timely
+fashion. If critical issues for a particular implementation exist at the time
+of a major release, support for that Ruby version may be dropped.
+</details>
+
+|     | Ruby OAuth 2 Version | Maintenance Branch | Supported Officially    | Supported Unofficially | Supported Incidentally |
+|:----|----------------------|--------------------|-------------------------|------------------------|------------------------|
+| 1️⃣ | 2.0.x (unreleased)   | `master`           | 2.7, 3.0, 3.1           | 2.6, 2.5               | 2.4, 2.3, 2.2          |
+| 2️⃣ | 1.4.x                | `1-4-stable`       | 2.5, 2.6, 2.7, 3.0, 3.1 | 2.1, 2.2, 2.3, 2.4     | 2.0, 1.9               |
+| 3️⃣ | older                | N/A                | Best of luck to you!    | Please upgrade!        |                        |
+
+NOTE: Once 2.0 is released, the 1.4 series will only receive critical bug and security updates.
+See [SECURITY.md][🚎sec-pol]
 
 ## Usage Examples
 
@@ -79,17 +266,42 @@ client.auth_code.authorize_url(:redirect_uri => 'http://localhost:8080/oauth2/ca
 # => "https://example.org/oauth/authorization?response_type=code&client_id=client_id&redirect_uri=http://localhost:8080/oauth2/callback"
 
 token = client.auth_code.get_token('authorization_code_value', :redirect_uri => 'http://localhost:8080/oauth2/callback', :headers => {'Authorization' => 'Basic some_password'})
-response = token.get('/api/resource', :params => { 'query_foo' => 'bar' })
+response = token.get('/api/resource', :params => {'query_foo' => 'bar'})
 response.class.name
 # => OAuth2::Response
 ```
+
+<details>
+  <summary>Debugging</summary>
+
+Set an environment variable, however you would [normally do that](https://github.com/bkeepers/dotenv).
+
+```ruby
+# will log both request and response, including bodies
+ENV['OAUTH_DEBUG'] = 'true'
+```
+
+By default, debug output will go to `$stdout`. This can be overridden when
+initializing your OAuth2::Client.
+
+```ruby
+require 'oauth2'
+client = OAuth2::Client.new(
+  'client_id',
+  'client_secret',
+  :site => 'https://example.org',
+  :logger => Logger.new('example.log', 'weekly')
+)
+```
+</details>
+
 ## OAuth2::Response
 
-The AccessToken methods #get, #post, #put and #delete and the generic #request
+The `AccessToken` methods `#get`, `#post`, `#put` and `#delete` and the generic `#request`
 will return an instance of the #OAuth2::Response class.
 
-This instance contains a #parsed method that will parse the response body and
-return a Hash if the Content-Type is application/x-www-form-urlencoded or if
+This instance contains a `#parsed` method that will parse the response body and
+return a Hash if the `Content-Type` is `application/x-www-form-urlencoded` or if
 the body is a JSON object.  It will return an Array if the body is a JSON
 array.  Otherwise, it will return the original body string.
 
@@ -99,27 +311,27 @@ respective methods.
 ## OAuth2::AccessToken
 
 If you have an existing Access Token for a user, you can initialize an instance
-using various class methods including the standard new, from_hash (if you have
-a hash of the values), or from_kvform (if you have an
-application/x-www-form-urlencoded encoded string of the values).
+using various class methods including the standard new, `from_hash` (if you have
+a hash of the values), or `from_kvform` (if you have an
+`application/x-www-form-urlencoded` encoded string of the values).
 
 ## OAuth2::Error
 
-On 400+ status code responses, an OAuth2::Error will be raised.  If it is a
-standard OAuth2 error response, the body will be parsed and #code and #description will contain the values provided from the error and
-error_description parameters.  The #response property of OAuth2::Error will
-always contain the OAuth2::Response instance.
+On 400+ status code responses, an `OAuth2::Error` will be raised.  If it is a
+standard OAuth2 error response, the body will be parsed and `#code` and `#description` will contain the values provided from the error and
+`error_description` parameters.  The `#response` property of `OAuth2::Error` will
+always contain the `OAuth2::Response` instance.
 
-If you do not want an error to be raised, you may use :raise_errors => false
-option on initialization of the client.  In this case the OAuth2::Response
+If you do not want an error to be raised, you may use `:raise_errors => false`
+option on initialization of the client.  In this case the `OAuth2::Response`
 instance will be returned as usual and on 400+ status code responses, the
-Response instance will contain the OAuth2::Error instance.
+Response instance will contain the `OAuth2::Error` instance.
 
 ## Authorization Grants
 
 Currently the Authorization Code, Implicit, Resource Owner Password Credentials, Client Credentials, and Assertion
 authentication grant types have helper strategy classes that simplify client
-use.  They are available via the #auth_code, #implicit, #password, #client_credentials, and #assertion methods respectively.
+use. They are available via the `#auth_code`, `#implicit`, `#password`, `#client_credentials`, and `#assertion` methods respectively.
 
 ```ruby
 auth_url = client.auth_code.authorize_url(:redirect_uri => 'http://localhost:8080/oauth/callback')
@@ -143,56 +355,9 @@ request, add a 'headers' hash under 'params':
 token = client.auth_code.get_token('code_value', :redirect_uri => 'http://localhost:8080/oauth/callback', :headers => {'Some' => 'Header'})
 ```
 
-You can always use the #request method on the OAuth2::Client instance to make
+You can always use the `#request` method on the `OAuth2::Client` instance to make
 requests for tokens for any Authentication grant type.
 
-## Supported Ruby Versions
-
-This library aims to support and is [tested against][travis] the following Ruby
-implementations:
-
-### Rubies with support ending at Oauth2 1.x
-
-* Ruby 1.9.3
-  - [JRuby 1.7][jruby-1.7] (targets MRI v1.9)
-
-* Ruby 2.0.0
-  - [JRuby 9.0][jruby-9.0] (targets MRI v2.0)
-* Ruby 2.1
-
----
-
-### Rubies with continued support past Oauth2 2.x
-
-* Ruby 2.2 - Support ends with version 2.x series
-* Ruby 2.3 - Support ends with version 3.x series
-  - [JRuby 9.1][jruby-9.1] (targets MRI v2.3) 
-* Ruby 2.4 - Support ends with version 4.x series
-* Ruby 2.5 - Support ends with version 5.x series
-  - [JRuby 9.2][jruby-9.2] (targets MRI v2.5)
-  - [truffleruby][truffleruby] (targets MRI 2.5)
-* Ruby 2.6 - Support ends with version 6.x series
-* Ruby 2.7 - Support ends with version 7.x series
-
-[jruby-1.7]: https://www.jruby.org/2017/05/11/jruby-1-7-27.html
-[jruby-9.0]: https://www.jruby.org/2016/01/26/jruby-9-0-5-0.html
-[jruby-9.1]: https://www.jruby.org/2017/05/16/jruby-9-1-9-0.html
-[jruby-9.2]: https://www.jruby.org/2018/05/24/jruby-9-2-0-0.html
-[truffleruby]: https://github.com/oracle/truffleruby
-
-If something doesn't work on one of these interpreters, it's a bug.
-
-This library may inadvertently work (or seem to work) on other Ruby
-implementations, however support will only be provided for the versions listed
-above.
-
-If you would like this library to support another Ruby version, you may
-volunteer to be a maintainer. Being a maintainer entails making sure all tests
-run and pass on that implementation. When something breaks on your
-implementation, you will be responsible for providing patches in a timely
-fashion. If critical issues for a particular implementation exist at the time
-of a major release, support for that Ruby version may be dropped.
-
 ## Versioning
 
 This library aims to adhere to [Semantic Versioning 2.0.0][semver].
@@ -216,21 +381,21 @@ spec.add_dependency 'oauth2', '~> 1.4'
 
 ## License
 
-[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)][source-license]
+[![License: MIT][🖇src-license-img]][🖇src-license]
 
 - Copyright (c) 2011-2013 Michael Bleigh and Intridea, Inc.
-- Copyright (c) 2017-2018 [oauth-xx organization][oauth-xx]
+- Copyright (c) 2017-2022 [oauth-xx organization][oauth-xx]
 - See [LICENSE][license] for details.
 
 [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Foauth-xx%2Foauth2.svg?type=large)][fossa2]
 
-[license]: LICENSE
+[license]: https://github.com/oauth-xx/oauth2/blob/master/LICENSE
 [oauth-xx]: https://github.com/oauth-xx
 [fossa2]: https://app.fossa.io/projects/git%2Bgithub.com%2Foauth-xx%2Foauth2?ref=badge_large
 
 ## Development
 
-After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
+After checking out the repo, run `bundle install` to install dependencies. Then, run `bundle excec rake spec` to run the tests.
 
 To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
 
diff --git a/Rakefile b/Rakefile
index 1066a1b..f8c96f3 100644
--- a/Rakefile
+++ b/Rakefile
@@ -1,15 +1,10 @@
 # encoding: utf-8
+# frozen_string_literal: true
 
 # !/usr/bin/env rake
 
 require 'bundler/gem_tasks'
 
-begin
-  require 'wwtd/tasks'
-rescue LoadError
-  puts 'failed to load wwtd'
-end
-
 begin
   require 'rspec/core/rake_task'
   RSpec::Core::RakeTask.new(:spec)
@@ -31,15 +26,15 @@ rescue LoadError
   end
 end
 
-namespace :doc do
-  require 'rdoc/task'
-  require File.expand_path('../lib/oauth2/version', __FILE__)
-  RDoc::Task.new do |rdoc|
-    rdoc.rdoc_dir = 'rdoc'
-    rdoc.title = "oauth2 #{OAuth2::Version}"
-    rdoc.main = 'README.md'
-    rdoc.rdoc_files.include('README.md', 'LICENSE.md', 'lib/**/*.rb')
-  end
-end
+# namespace :doc do
+#   require 'rdoc/task'
+#   require 'oauth2/version'
+#   RDoc::Task.new do |rdoc|
+#     rdoc.rdoc_dir = 'rdoc'
+#     rdoc.title = "oauth2 #{OAuth2::Version}"
+#     rdoc.main = 'README.md'
+#     rdoc.rdoc_files.include('README.md', 'LICENSE.md', 'lib/**/*.rb')
+#   end
+# end
 
 task :default => [:test, :rubocop]
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..9e7d133
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,15 @@
+# Security Policy
+
+## Supported Versions
+
+| Version      | Supported |
+|--------------|-----------|
+| 2.0.<latest> | ✅         |
+| 1.4.<latest> | ✅         |
+| older        | ⛔️        |
+
+## Reporting a Vulnerability
+
+Peter Boling is the primary maintainer of this gem. Please find a way
+to [contact him directly](https://railsbling.com/contact) to report the issue. Include as much relevant information as
+possible.
diff --git a/debian/changelog b/debian/changelog
index b7f3f9c..8bf8025 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+ruby-oauth2 (1.4.9-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Sun, 15 May 2022 14:44:20 -0000
+
 ruby-oauth2 (1.4.4-1) unstable; urgency=medium
 
   [ Utkarsh Gupta ]
diff --git a/docs/images/logo/README.txt b/docs/images/logo/README.txt
new file mode 100644
index 0000000..bb40555
--- /dev/null
+++ b/docs/images/logo/README.txt
@@ -0,0 +1,15 @@
+The OAuth 2.0 Logo - oauth2-logo-124px.png (resized)
+
+https://oauth.net/about/credits/
+
+The OAuth logo was designed by Chris Messina.
+
+---
+
+The Ruby Logo - ruby-logo-124px.jpeg (resized)
+
+https://www.ruby-lang.org/en/about/logo/
+
+Yukihiro Matsumoto, Ruby Visual Identity Team, CC BY-SA 2.5
+
+https://creativecommons.org/licenses/by-sa/2.5
diff --git a/docs/images/logo/oauth2-logo-124px.png b/docs/images/logo/oauth2-logo-124px.png
new file mode 100644
index 0000000..41a8d35
Binary files /dev/null and b/docs/images/logo/oauth2-logo-124px.png differ
diff --git a/docs/images/logo/ruby-logo-198px.svg b/docs/images/logo/ruby-logo-198px.svg
new file mode 100644
index 0000000..59cf324
--- /dev/null
+++ b/docs/images/logo/ruby-logo-198px.svg
@@ -0,0 +1,948 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Generator: Adobe Illustrator 12.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 51448)  -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   version="1.1"
+   id="Layer_1"
+   width="198.13"
+   height="197.58"
+   viewBox="0 0 198.13 197.58"
+   overflow="visible"
+   enable-background="new 0 0 198.13 197.58"
+   xml:space="preserve"
+   inkscape:version="0.48.0 r9654"
+   sodipodi:docname="Logo-Ruby.svg"><metadata
+   id="metadata3320"><rdf:RDF><cc:Work
+       rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+         rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
+   id="defs3489"><linearGradient
+     id="XMLID_17_-9"
+     gradientUnits="userSpaceOnUse"
+     x1="174.0737"
+     y1="215.5488"
+     x2="132.27631"
+     y2="141.7533"><stop
+       offset="0"
+       style="stop-color:#FB7655"
+       id="stop3272-4" /><stop
+       offset="0"
+       style="stop-color:#FB7655"
+       id="stop3274-5" /><stop
+       offset="0.41"
+       style="stop-color:#E42B1E"
+       id="stop3276-1" /><stop
+       offset="0.99"
+       style="stop-color:#990000"
+       id="stop3278-0" /><stop
+       offset="1"
+       style="stop-color:#990000"
+       id="stop3280-3" /></linearGradient><linearGradient
+     id="XMLID_18_-7"
+     gradientUnits="userSpaceOnUse"
+     x1="194.895"
+     y1="153.5576"
+     x2="141.0276"
+     y2="117.4093"><stop
+       offset="0"
+       style="stop-color:#871101"
+       id="stop3285-8" /><stop
+       offset="0"
+       style="stop-color:#871101"
+       id="stop3287-8" /><stop
+       offset="0.99"
+       style="stop-color:#911209"
+       id="stop3289-6" /><stop
+       offset="1"
+       style="stop-color:#911209"
+       id="stop3291-0" /></linearGradient><linearGradient
+     id="XMLID_19_-4"
+     gradientUnits="userSpaceOnUse"
+     x1="151.79539"
+     y1="217.7852"
+     x2="97.929703"
+     y2="181.638"><stop
+       offset="0"
+       style="stop-color:#871101"
+       id="stop3296-6" /><stop
+       offset="0"
+       style="stop-color:#871101"
+       id="stop3298-7" /><stop
+       offset="0.99"
+       style="stop-color:#911209"
+       id="stop3300-6" /><stop
+       offset="1"
+       style="stop-color:#911209"
+       id="stop3302-0" /></linearGradient><linearGradient
+     id="XMLID_20_-9"
+     gradientUnits="userSpaceOnUse"
+     x1="38.696301"
+     y1="127.3906"
+     x2="47.046902"
+     y2="181.66141"><stop
+       offset="0"
+       style="stop-color:#FFFFFF"
+       id="stop3307-7" /><stop
+       offset="0"
+       style="stop-color:#FFFFFF"
+       id="stop3309-5" /><stop
+       offset="0.23"
+       style="stop-color:#E57252"
+       id="stop3311-9" /><stop
+       offset="0.46"
+       style="stop-color:#DE3B20"
+       id="stop3313-7" /><stop
+       offset="0.99"
+       style="stop-color:#A60003"
+       id="stop3315-8" /><stop
+       offset="1"
+       style="stop-color:#A60003"
+       id="stop3317-5" /></linearGradient><linearGradient
+     id="XMLID_21_-3"
+     gradientUnits="userSpaceOnUse"
+     x1="96.132797"
+     y1="76.715302"
+     x2="99.209602"
+     y2="132.1021"><stop
+       offset="0"
+       style="stop-color:#FFFFFF"
+       id="stop3322-3" /><stop
+       offset="0"
+       style="stop-color:#FFFFFF"
+       id="stop3324-8" /><stop
+       offset="0.23"
+       style="stop-color:#E4714E"
+       id="stop3326-3" /><stop
+       offset="0.56"
+       style="stop-color:#BE1A0D"
+       id="stop3328-7" /><stop
+       offset="0.99"
+       style="stop-color:#A80D00"
+       id="stop3330-9" /><stop
+       offset="1"
+       style="stop-color:#A80D00"
+       id="stop3332-3" /></linearGradient><linearGradient
+     id="XMLID_22_-7"
+     gradientUnits="userSpaceOnUse"
+     x1="147.103"
+     y1="25.521"
+     x2="156.3141"
+     y2="65.216202"><stop
+       offset="0"
+       style="stop-color:#FFFFFF"
+       id="stop3337-8" /><stop
+       offset="0"
+       style="stop-color:#FFFFFF"
+       id="stop3339-7" /><stop
+       offset="0.18"
+       style="stop-color:#E46342"
+       id="stop3341-4" /><stop
+       offset="0.4"
+       style="stop-color:#C82410"
+       id="stop3343-1" /><stop
+       offset="0.99"
+       style="stop-color:#A80D00"
+       id="stop3345-9" /><stop
+       offset="1"
+       style="stop-color:#A80D00"
+       id="stop3347-0" /></linearGradient><linearGradient
+     id="XMLID_23_-9"
+     gradientUnits="userSpaceOnUse"
+     x1="118.9761"
+     y1="11.5415"
+     x2="158.66859"
+     y2="-8.3048"><stop
+       offset="0"
+       style="stop-color:#FFFFFF"
+       id="stop3352-8" /><stop
+       offset="0"
+       style="stop-color:#FFFFFF"
+       id="stop3354-8" /><stop
+       offset="0.54"
+       style="stop-color:#C81F11"
+       id="stop3356-5" /><stop
+       offset="0.99"
+       style="stop-color:#BF0905"
+       id="stop3358-8" /><stop
+       offset="1"
+       style="stop-color:#BF0905"
+       id="stop3360-4" /></linearGradient><linearGradient
+     id="XMLID_24_-3"
+     gradientUnits="userSpaceOnUse"
+     x1="3.9033"
+     y1="113.5547"
+     x2="7.1701999"
+     y2="146.2628"><stop
+       offset="0"
+       style="stop-color:#FFFFFF"
+       id="stop3365-7" /><stop
+       offset="0"
+       style="stop-color:#FFFFFF"
+       id="stop3367-1" /><stop
+       offset="0.31"
+       style="stop-color:#DE4024"
+       id="stop3369-3" /><stop
+       offset="0.99"
+       style="stop-color:#BF190B"
+       id="stop3371-8" /><stop
+       offset="1"
+       style="stop-color:#BF190B"
+       id="stop3373-0" /></linearGradient><linearGradient
+     id="XMLID_25_-9"
+     gradientUnits="userSpaceOnUse"
+     x1="-18.5557"
+     y1="155.10451"
+     x2="135.0152"
+     y2="-2.8092999"><stop
+       offset="0"
+       style="stop-color:#BD0012"
+       id="stop3380-7" /><stop
+       offset="0"
+       style="stop-color:#BD0012"
+       id="stop3382-9" /><stop
+       offset="0.07"
+       style="stop-color:#FFFFFF"
+       id="stop3384-9" /><stop
+       offset="0.17"
+       style="stop-color:#FFFFFF"
+       id="stop3386-3" /><stop
+       offset="0.27"
+       style="stop-color:#C82F1C"
+       id="stop3388-2" /><stop
+       offset="0.33"
+       style="stop-color:#820C01"
+       id="stop3390-4" /><stop
+       offset="0.46"
+       style="stop-color:#A31601"
+       id="stop3392-3" /><stop
+       offset="0.72"
+       style="stop-color:#B31301"
+       id="stop3394-7" /><stop
+       offset="0.99"
+       style="stop-color:#E82609"
+       id="stop3396-1" /><stop
+       offset="1"
+       style="stop-color:#E82609"
+       id="stop3398-2" /></linearGradient><linearGradient
+     id="XMLID_26_-2"
+     gradientUnits="userSpaceOnUse"
+     x1="99.074699"
+     y1="171.0332"
+     x2="52.817699"
+     y2="159.61659"><stop
+       offset="0"
+       style="stop-color:#8C0C01"
+       id="stop3403-0" /><stop
+       offset="0"
+       style="stop-color:#8C0C01"
+       id="stop3405-2" /><stop
+       offset="0.54"
+       style="stop-color:#990C00"
+       id="stop3407-1" /><stop
+       offset="0.99"
+       style="stop-color:#A80D0E"
+       id="stop3409-7" /><stop
+       offset="1"
+       style="stop-color:#A80D0E"
+       id="stop3411-5" /></linearGradient><linearGradient
+     id="XMLID_27_-1"
+     gradientUnits="userSpaceOnUse"
+     x1="178.52589"
+     y1="115.5146"
+     x2="137.43269"
+     y2="78.683998"><stop
+       offset="0"
+       style="stop-color:#7E110B"
+       id="stop3416-7" /><stop
+       offset="0"
+       style="stop-color:#7E110B"
+       id="stop3418-4" /><stop
+       offset="0.99"
+       style="stop-color:#9E0C00"
+       id="stop3420-1" /><stop
+       offset="1"
+       style="stop-color:#9E0C00"
+       id="stop3422-7" /></linearGradient><linearGradient
+     id="XMLID_28_-1"
+     gradientUnits="userSpaceOnUse"
+     x1="193.6235"
+     y1="47.937"
+     x2="173.15421"
+     y2="26.053801"><stop
+       offset="0"
+       style="stop-color:#79130D"
+       id="stop3427-1" /><stop
+       offset="0"
+       style="stop-color:#79130D"
+       id="stop3429-1" /><stop
+       offset="0.99"
+       style="stop-color:#9E120B"
+       id="stop3431-7" /><stop
+       offset="1"
+       style="stop-color:#9E120B"
+       id="stop3433-0" /></linearGradient><radialGradient
+     id="XMLID_29_-4"
+     cx="143.8315"
+     cy="79.388199"
+     r="50.357601"
+     gradientUnits="userSpaceOnUse"><stop
+       offset="0"
+       style="stop-color:#A80D00"
+       id="stop3440-0" /><stop
+       offset="0"
+       style="stop-color:#A80D00"
+       id="stop3442-8" /><stop
+       offset="0.99"
+       style="stop-color:#7E0E08"
+       id="stop3444-5" /><stop
+       offset="1"
+       style="stop-color:#7E0E08"
+       id="stop3446-1" /></radialGradient><radialGradient
+     id="XMLID_30_-6"
+     cx="74.0923"
+     cy="145.75101"
+     r="66.943703"
+     gradientUnits="userSpaceOnUse"><stop
+       offset="0"
+       style="stop-color:#A30C00"
+       id="stop3451-6" /><stop
+       offset="0"
+       style="stop-color:#A30C00"
+       id="stop3453-2" /><stop
+       offset="0.99"
+       style="stop-color:#800E08"
+       id="stop3455-1" /><stop
+       offset="1"
+       style="stop-color:#800E08"
+       id="stop3457-9" /></radialGradient><linearGradient
+     id="XMLID_31_-6"
+     gradientUnits="userSpaceOnUse"
+     x1="26.669901"
+     y1="197.33591"
+     x2="9.9886999"
+     y2="140.742"><stop
+       offset="0"
+       style="stop-color:#8B2114"
+       id="stop3462-4" /><stop
+       offset="0"
+       style="stop-color:#8B2114"
+       id="stop3464-8" /><stop
+       offset="0.43"
+       style="stop-color:#9E100A"
+       id="stop3466-0" /><stop
+       offset="0.99"
+       style="stop-color:#B3100C"
+       id="stop3468-8" /><stop
+       offset="1"
+       style="stop-color:#B3100C"
+       id="stop3470-1" /></linearGradient><linearGradient
+     id="XMLID_32_-0"
+     gradientUnits="userSpaceOnUse"
+     x1="154.6411"
+     y1="9.7979002"
+     x2="192.039"
+     y2="26.305901"><stop
+       offset="0"
+       style="stop-color:#B31000"
+       id="stop3475-2" /><stop
+       offset="0"
+       style="stop-color:#B31000"
+       id="stop3477-2" /><stop
+       offset="0.44"
+       style="stop-color:#910F08"
+       id="stop3479-9" /><stop
+       offset="0.99"
+       style="stop-color:#791C12"
+       id="stop3481-7" /><stop
+       offset="1"
+       style="stop-color:#791C12"
+       id="stop3483-5" /></linearGradient><linearGradient
+     y2="141.7533"
+     x2="132.27631"
+     y1="215.5488"
+     x1="174.0737"
+     gradientUnits="userSpaceOnUse"
+     id="linearGradient3448"
+     xlink:href="#XMLID_17_-9"
+     inkscape:collect="always" /></defs><sodipodi:namedview
+   pagecolor="#ffffff"
+   bordercolor="#666666"
+   borderopacity="1"
+   objecttolerance="10"
+   gridtolerance="10"
+   guidetolerance="10"
+   inkscape:pageopacity="0"
+   inkscape:pageshadow="2"
+   inkscape:window-width="1036"
+   inkscape:window-height="1124"
+   id="namedview3487"
+   showgrid="false"
+   inkscape:zoom="1.3412289"
+   inkscape:cx="99.065005"
+   inkscape:cy="98.790004"
+   inkscape:window-x="882"
+   inkscape:window-y="24"
+   inkscape:window-maximized="0"
+   inkscape:current-layer="Layer_1" />
+<linearGradient
+   id="XMLID_17_"
+   gradientUnits="userSpaceOnUse"
+   x1="174.0737"
+   y1="215.5488"
+   x2="132.2763"
+   y2="141.7533">
+	<stop
+   offset="0"
+   style="stop-color:#FB7655"
+   id="stop3272" />
+	<stop
+   offset="0"
+   style="stop-color:#FB7655"
+   id="stop3274" />
+	<stop
+   offset="0.41"
+   style="stop-color:#E42B1E"
+   id="stop3276" />
+	<stop
+   offset="0.99"
+   style="stop-color:#990000"
+   id="stop3278" />
+	<stop
+   offset="1"
+   style="stop-color:#990000"
+   id="stop3280" />
+</linearGradient>
+
+<linearGradient
+   id="XMLID_18_"
+   gradientUnits="userSpaceOnUse"
+   x1="194.895"
+   y1="153.5576"
+   x2="141.0276"
+   y2="117.4093">
+	<stop
+   offset="0"
+   style="stop-color:#871101"
+   id="stop3285" />
+	<stop
+   offset="0"
+   style="stop-color:#871101"
+   id="stop3287" />
+	<stop
+   offset="0.99"
+   style="stop-color:#911209"
+   id="stop3289" />
+	<stop
+   offset="1"
+   style="stop-color:#911209"
+   id="stop3291" />
+</linearGradient>
+
+<linearGradient
+   id="XMLID_19_"
+   gradientUnits="userSpaceOnUse"
+   x1="151.7954"
+   y1="217.7852"
+   x2="97.9297"
+   y2="181.638">
+	<stop
+   offset="0"
+   style="stop-color:#871101"
+   id="stop3296" />
+	<stop
+   offset="0"
+   style="stop-color:#871101"
+   id="stop3298" />
+	<stop
+   offset="0.99"
+   style="stop-color:#911209"
+   id="stop3300" />
+	<stop
+   offset="1"
+   style="stop-color:#911209"
+   id="stop3302" />
+</linearGradient>
+
+<linearGradient
+   id="XMLID_20_"
+   gradientUnits="userSpaceOnUse"
+   x1="38.6963"
+   y1="127.3906"
+   x2="47.0469"
+   y2="181.6614">
+	<stop
+   offset="0"
+   style="stop-color:#FFFFFF"
+   id="stop3307" />
+	<stop
+   offset="0"
+   style="stop-color:#FFFFFF"
+   id="stop3309" />
+	<stop
+   offset="0.23"
+   style="stop-color:#E57252"
+   id="stop3311" />
+	<stop
+   offset="0.46"
+   style="stop-color:#DE3B20"
+   id="stop3313" />
+	<stop
+   offset="0.99"
+   style="stop-color:#A60003"
+   id="stop3315" />
+	<stop
+   offset="1"
+   style="stop-color:#A60003"
+   id="stop3317" />
+</linearGradient>
+
+<linearGradient
+   id="XMLID_21_"
+   gradientUnits="userSpaceOnUse"
+   x1="96.1328"
+   y1="76.7153"
+   x2="99.2096"
+   y2="132.1021">
+	<stop
+   offset="0"
+   style="stop-color:#FFFFFF"
+   id="stop3322" />
+	<stop
+   offset="0"
+   style="stop-color:#FFFFFF"
+   id="stop3324" />
+	<stop
+   offset="0.23"
+   style="stop-color:#E4714E"
+   id="stop3326" />
+	<stop
+   offset="0.56"
+   style="stop-color:#BE1A0D"
+   id="stop3328" />
+	<stop
+   offset="0.99"
+   style="stop-color:#A80D00"
+   id="stop3330" />
+	<stop
+   offset="1"
+   style="stop-color:#A80D00"
+   id="stop3332" />
+</linearGradient>
+
+<linearGradient
+   id="XMLID_22_"
+   gradientUnits="userSpaceOnUse"
+   x1="147.103"
+   y1="25.521"
+   x2="156.3141"
+   y2="65.2162">
+	<stop
+   offset="0"
+   style="stop-color:#FFFFFF"
+   id="stop3337" />
+	<stop
+   offset="0"
+   style="stop-color:#FFFFFF"
+   id="stop3339" />
+	<stop
+   offset="0.18"
+   style="stop-color:#E46342"
+   id="stop3341" />
+	<stop
+   offset="0.4"
+   style="stop-color:#C82410"
+   id="stop3343" />
+	<stop
+   offset="0.99"
+   style="stop-color:#A80D00"
+   id="stop3345" />
+	<stop
+   offset="1"
+   style="stop-color:#A80D00"
+   id="stop3347" />
+</linearGradient>
+
+<linearGradient
+   id="XMLID_23_"
+   gradientUnits="userSpaceOnUse"
+   x1="118.9761"
+   y1="11.5415"
+   x2="158.6686"
+   y2="-8.3048">
+	<stop
+   offset="0"
+   style="stop-color:#FFFFFF"
+   id="stop3352" />
+	<stop
+   offset="0"
+   style="stop-color:#FFFFFF"
+   id="stop3354" />
+	<stop
+   offset="0.54"
+   style="stop-color:#C81F11"
+   id="stop3356" />
+	<stop
+   offset="0.99"
+   style="stop-color:#BF0905"
+   id="stop3358" />
+	<stop
+   offset="1"
+   style="stop-color:#BF0905"
+   id="stop3360" />
+</linearGradient>
+
+<linearGradient
+   id="XMLID_24_"
+   gradientUnits="userSpaceOnUse"
+   x1="3.9033"
+   y1="113.5547"
+   x2="7.1702"
+   y2="146.2628">
+	<stop
+   offset="0"
+   style="stop-color:#FFFFFF"
+   id="stop3365" />
+	<stop
+   offset="0"
+   style="stop-color:#FFFFFF"
+   id="stop3367" />
+	<stop
+   offset="0.31"
+   style="stop-color:#DE4024"
+   id="stop3369" />
+	<stop
+   offset="0.99"
+   style="stop-color:#BF190B"
+   id="stop3371" />
+	<stop
+   offset="1"
+   style="stop-color:#BF190B"
+   id="stop3373" />
+</linearGradient>
+
+
+<linearGradient
+   id="XMLID_25_"
+   gradientUnits="userSpaceOnUse"
+   x1="-18.5557"
+   y1="155.1045"
+   x2="135.0152"
+   y2="-2.8093">
+	<stop
+   offset="0"
+   style="stop-color:#BD0012"
+   id="stop3380" />
+	<stop
+   offset="0"
+   style="stop-color:#BD0012"
+   id="stop3382" />
+	<stop
+   offset="0.07"
+   style="stop-color:#FFFFFF"
+   id="stop3384" />
+	<stop
+   offset="0.17"
+   style="stop-color:#FFFFFF"
+   id="stop3386" />
+	<stop
+   offset="0.27"
+   style="stop-color:#C82F1C"
+   id="stop3388" />
+	<stop
+   offset="0.33"
+   style="stop-color:#820C01"
+   id="stop3390" />
+	<stop
+   offset="0.46"
+   style="stop-color:#A31601"
+   id="stop3392" />
+	<stop
+   offset="0.72"
+   style="stop-color:#B31301"
+   id="stop3394" />
+	<stop
+   offset="0.99"
+   style="stop-color:#E82609"
+   id="stop3396" />
+	<stop
+   offset="1"
+   style="stop-color:#E82609"
+   id="stop3398" />
+</linearGradient>
+
+<linearGradient
+   id="XMLID_26_"
+   gradientUnits="userSpaceOnUse"
+   x1="99.0747"
+   y1="171.0332"
+   x2="52.8177"
+   y2="159.6166">
+	<stop
+   offset="0"
+   style="stop-color:#8C0C01"
+   id="stop3403" />
+	<stop
+   offset="0"
+   style="stop-color:#8C0C01"
+   id="stop3405" />
+	<stop
+   offset="0.54"
+   style="stop-color:#990C00"
+   id="stop3407" />
+	<stop
+   offset="0.99"
+   style="stop-color:#A80D0E"
+   id="stop3409" />
+	<stop
+   offset="1"
+   style="stop-color:#A80D0E"
+   id="stop3411" />
+</linearGradient>
+
+<linearGradient
+   id="XMLID_27_"
+   gradientUnits="userSpaceOnUse"
+   x1="178.5259"
+   y1="115.5146"
+   x2="137.4327"
+   y2="78.684">
+	<stop
+   offset="0"
+   style="stop-color:#7E110B"
+   id="stop3416" />
+	<stop
+   offset="0"
+   style="stop-color:#7E110B"
+   id="stop3418" />
+	<stop
+   offset="0.99"
+   style="stop-color:#9E0C00"
+   id="stop3420" />
+	<stop
+   offset="1"
+   style="stop-color:#9E0C00"
+   id="stop3422" />
+</linearGradient>
+
+<linearGradient
+   id="XMLID_28_"
+   gradientUnits="userSpaceOnUse"
+   x1="193.6235"
+   y1="47.937"
+   x2="173.1542"
+   y2="26.0538">
+	<stop
+   offset="0"
+   style="stop-color:#79130D"
+   id="stop3427" />
+	<stop
+   offset="0"
+   style="stop-color:#79130D"
+   id="stop3429" />
+	<stop
+   offset="0.99"
+   style="stop-color:#9E120B"
+   id="stop3431" />
+	<stop
+   offset="1"
+   style="stop-color:#9E120B"
+   id="stop3433" />
+</linearGradient>
+
+
+<radialGradient
+   id="XMLID_29_"
+   cx="143.8315"
+   cy="79.3882"
+   r="50.3576"
+   gradientUnits="userSpaceOnUse">
+	<stop
+   offset="0"
+   style="stop-color:#A80D00"
+   id="stop3440" />
+	<stop
+   offset="0"
+   style="stop-color:#A80D00"
+   id="stop3442" />
+	<stop
+   offset="0.99"
+   style="stop-color:#7E0E08"
+   id="stop3444" />
+	<stop
+   offset="1"
+   style="stop-color:#7E0E08"
+   id="stop3446" />
+</radialGradient>
+
+<radialGradient
+   id="XMLID_30_"
+   cx="74.0923"
+   cy="145.751"
+   r="66.9437"
+   gradientUnits="userSpaceOnUse">
+	<stop
+   offset="0"
+   style="stop-color:#A30C00"
+   id="stop3451" />
+	<stop
+   offset="0"
+   style="stop-color:#A30C00"
+   id="stop3453" />
+	<stop
+   offset="0.99"
+   style="stop-color:#800E08"
+   id="stop3455" />
+	<stop
+   offset="1"
+   style="stop-color:#800E08"
+   id="stop3457" />
+</radialGradient>
+
+<linearGradient
+   id="XMLID_31_"
+   gradientUnits="userSpaceOnUse"
+   x1="26.6699"
+   y1="197.3359"
+   x2="9.9887"
+   y2="140.742">
+	<stop
+   offset="0"
+   style="stop-color:#8B2114"
+   id="stop3462" />
+	<stop
+   offset="0"
+   style="stop-color:#8B2114"
+   id="stop3464" />
+	<stop
+   offset="0.43"
+   style="stop-color:#9E100A"
+   id="stop3466" />
+	<stop
+   offset="0.99"
+   style="stop-color:#B3100C"
+   id="stop3468" />
+	<stop
+   offset="1"
+   style="stop-color:#B3100C"
+   id="stop3470" />
+</linearGradient>
+
+<linearGradient
+   id="XMLID_32_"
+   gradientUnits="userSpaceOnUse"
+   x1="154.6411"
+   y1="9.7979"
+   x2="192.039"
+   y2="26.3059">
+	<stop
+   offset="0"
+   style="stop-color:#B31000"
+   id="stop3475" />
+	<stop
+   offset="0"
+   style="stop-color:#B31000"
+   id="stop3477" />
+	<stop
+   offset="0.44"
+   style="stop-color:#910F08"
+   id="stop3479" />
+	<stop
+   offset="0.99"
+   style="stop-color:#791C12"
+   id="stop3481" />
+	<stop
+   offset="1"
+   style="stop-color:#791C12"
+   id="stop3483" />
+</linearGradient>
+
+<polygon
+   style="fill:url(#linearGradient3448);fill-rule:evenodd"
+   clip-rule="evenodd"
+   points="153.5,130.41 40.38,197.58 186.849,187.641 198.13,39.95 "
+   id="polygon3282" /><polygon
+   style="fill:url(#XMLID_18_-7);fill-rule:evenodd"
+   clip-rule="evenodd"
+   points="187.089,187.54 174.5,100.65 140.209,145.93 "
+   id="polygon3293" /><polygon
+   style="fill:url(#XMLID_19_-4);fill-rule:evenodd"
+   clip-rule="evenodd"
+   points="187.259,187.54 95.03,180.3 40.87,197.391 "
+   id="polygon3304" /><polygon
+   style="fill:url(#XMLID_20_-9);fill-rule:evenodd"
+   clip-rule="evenodd"
+   points="41,197.41 64.04,121.93 13.34,132.771 "
+   id="polygon3319" /><polygon
+   style="fill:url(#XMLID_21_-3);fill-rule:evenodd"
+   clip-rule="evenodd"
+   points="140.2,146.18 119,63.14 58.33,120.01 "
+   id="polygon3334" /><polygon
+   style="fill:url(#XMLID_22_-7);fill-rule:evenodd"
+   clip-rule="evenodd"
+   points="193.32,64.31 135.97,17.47 120,69.1 "
+   id="polygon3349" /><polygon
+   style="fill:url(#XMLID_23_-9);fill-rule:evenodd"
+   clip-rule="evenodd"
+   points="166.5,0.77 132.77,19.41 111.49,0.52 "
+   id="polygon3362" /><polygon
+   style="fill:url(#XMLID_24_-3);fill-rule:evenodd"
+   clip-rule="evenodd"
+   points="0,158.09 14.13,132.32 2.7,101.62 "
+   id="polygon3375" /><path
+   style="fill:#ffffff;fill-rule:evenodd"
+   inkscape:connector-curvature="0"
+   clip-rule="evenodd"
+   d="m 1.94,100.65 11.5,32.62 49.97,-11.211 57.05,-53.02 L 136.56,17.9 111.209,0 68.109,16.13 C 54.53,28.76 28.18,53.75 27.23,54.22 26.29,54.7 9.83,85.81 1.94,100.65 z"
+   id="path3377" /><path
+   style="fill:url(#XMLID_25_-9);fill-rule:evenodd"
+   inkscape:connector-curvature="0"
+   clip-rule="evenodd"
+   d="m 42.32,42.05 c 29.43,-29.18 67.37,-46.42 81.93,-31.73 14.551,14.69 -0.88,50.39 -30.31,79.56 -29.43,29.17 -66.9,47.36 -81.45,32.67 -14.56,-14.68 0.4,-51.33 29.83,-80.5 z"
+   id="path3400" /><path
+   style="fill:url(#XMLID_26_-2);fill-rule:evenodd"
+   inkscape:connector-curvature="0"
+   clip-rule="evenodd"
+   d="m 41,197.38 22.86,-75.72 75.92,24.39 C 112.33,171.79 81.8,193.55 41,197.38 z"
+   id="path3413" /><path
+   style="fill:url(#XMLID_27_-1);fill-rule:evenodd"
+   inkscape:connector-curvature="0"
+   clip-rule="evenodd"
+   d="m 120.56,68.89 19.49,77.2 C 162.98,121.98 183.56,96.06 193.639,64 l -73.079,4.89 z"
+   id="path3424" /><path
+   style="fill:url(#XMLID_28_-1);fill-rule:evenodd"
+   inkscape:connector-curvature="0"
+   clip-rule="evenodd"
+   d="m 193.44,64.39 c 7.8,-23.54 9.6,-57.31 -27.181,-63.58 l -30.18,16.67 57.361,46.91 z"
+   id="path3435" /><path
+   style="fill:#9e1209;fill-rule:evenodd"
+   inkscape:connector-curvature="0"
+   clip-rule="evenodd"
+   d="m 0,157.75 c 1.08,38.851 29.11,39.43 41.05,39.771 L 13.47,133.11 0,157.75 z"
+   id="path3437" /><path
+   style="fill:url(#XMLID_29_-4);fill-rule:evenodd"
+   inkscape:connector-curvature="0"
+   clip-rule="evenodd"
+   d="m 120.669,69.01 c 17.62,10.83 53.131,32.58 53.851,32.98 1.119,0.63 15.31,-23.93 18.53,-37.81 l -72.381,4.83 z"
+   id="path3448" /><path
+   style="fill:url(#XMLID_30_-6);fill-rule:evenodd"
+   inkscape:connector-curvature="0"
+   clip-rule="evenodd"
+   d="m 63.83,121.66 30.56,58.96 c 18.07,-9.8 32.22,-21.74 45.18,-34.53 L 63.83,121.66 z"
+   id="path3459" /><path
+   style="fill:url(#XMLID_31_-6);fill-rule:evenodd"
+   inkscape:connector-curvature="0"
+   clip-rule="evenodd"
+   d="m 13.35,133.19 -4.33,51.56 c 8.17,11.16 19.41,12.13 31.2,11.26 -8.53,-21.23 -25.57,-63.68 -26.87,-62.82 z"
+   id="path3472" /><path
+   style="fill:url(#XMLID_32_-0);fill-rule:evenodd"
+   inkscape:connector-curvature="0"
+   clip-rule="evenodd"
+   d="m 135.9,17.61 60.71,8.52 C 193.37,12.4 183.42,3.54 166.46,0.77 L 135.9,17.61 z"
+   id="path3485" /></svg>
\ No newline at end of file
diff --git a/gemfiles/README.md b/gemfiles/README.md
new file mode 100644
index 0000000..1ac3a71
--- /dev/null
+++ b/gemfiles/README.md
@@ -0,0 +1,106 @@
+# History
+
+`faraday` v0.17.3 is the first version that stops using `&Proc.new` for block forwarding,
+   and thus is the oldest version oauth2 is compatible with.
+
+```ruby
+gem 'faraday', ['>= 0.17.3', '< 3.0']
+```
+
+# Ruby
+
+We use the Github Action `ruby/setup-ruby@master` to install Ruby, and it has a matrix of
+[supported versions](https://github.com/ruby/setup-ruby/blob/master/README.md#supported-versions) (copied below).
+
+| Interpreter           | Versions                                                                                 |
+|-----------------------|------------------------------------------------------------------------------------------|
+| `ruby`                | 1.9.3, 2.0.0, 2.1.9, 2.2, all versions from 2.3.0 until 3.1.1, head, debug, mingw, mswin |
+| `jruby`               | 9.1.17.0 - 9.3.3.0, head                                                                 |
+| `truffleruby`         | 19.3.0 - 22.0.0, head                                                                    |
+| `truffleruby+graalvm` | 21.2.0 - 22.0.0, head                                                                    |
+
+In the naming of gemfiles, we will use the below shorthand for interpreter,
+and version. Platforms will be represented without modification.
+
+| Interpreter           | Shorthand |
+|-----------------------|-----------|
+| `ruby`                | r         |
+| `jruby`               | jr        |
+| `truffleruby`         | tr        |
+| `truffleruby+graalvm` | trg       |
+
+Building onto that, we can add the MRI target spec,
+since that's what all Rubygems use for minimum version compatibility.
+
+| Interpreter + Version      | MRI spec | Shorthand  |
+|----------------------------|----------|------------|
+| ruby-1.9.3                 | 1.9      | r1_9       |
+| ruby-2.0.0                 | 2.0      | r2_0       |
+| ruby-2.1.9                 | 2.1      | r2_1       |
+| ruby-2.2.x                 | 2.2      | r2_2       |
+| ruby-2.3.x                 | 2.3      | r2_3       |
+| ruby-2.4.x                 | 2.4      | r2_4       |
+| ruby-2.5.x                 | 2.5      | r2_5       |
+| ruby-2.6.x                 | 2.6      | r2_6       |
+| ruby-2.7.x                 | 2.7      | r2_7       |
+| ruby-3.0.x                 | 3.0      | r3_0       |
+| ruby-3.1.x                 | 3.1      | r3_1       |
+| ruby-head                  | 3.2      | rH3_2      |
+| ruby-mingw                 | (?)      | rmin       |
+| ruby-mswin                 | (?)      | rMS        |
+| jruby-9.1.x.x              | 2.3      | jr9_1-r2_3 |
+| jruby-9.2.x.x              | 2.5      | jr9_2-r2_5 |
+| jruby-9.3.x.x              | 2.6      | jr9_3-r2_6 |
+| jruby-head                 | 2.7      | jrH-r2_7   |
+| truffleruby-19.3.x         | 2.5(?)   | tr19-r2_5  |
+| truffleruby-20.x.x         | 2.6.5    | tr20-r2_6  |
+| truffleruby-21.x.x         | 2.7.4    | tr21-r2_7  |
+| truffleruby-22.x.x         | 3.0.2    | tr22-r3_0  |
+| truffleruby-head           | 3.1(?)   | trH-r3_1   |
+| truffleruby+graalvm-21.2.x | 2.7.4    | trg21-r2_7 |
+| truffleruby+graalvm-22.x.x | 3.0.2    | trg22-r3_0 |
+| truffleruby+graalvm-head   | 3.1(?)   | trgH-r3_1  |
+
+We will run tests on as many of these as possible, in a matrix with each supported major version of `faraday`,
+which means 0.17.3+ (as `f0`), 1.10.x (as `f1`), 2.2.x (as `f2`).
+
+Discrete versions of `faraday` to test against, as of 2022.02.19, with minimum version of Ruby for each:
+
+* 2.2.0, Ruby >= 2.6
+* 1.10.0, Ruby >= 2.4
+* 0.17.4, Ruby >= 1.9
+
+❌ - Incompatible
+✅ - Official Support
+🚧 - Unofficial Support
+🤡 - Incidental Compatibility
+🙈 - Unknown Compatibility
+
+| Shorthand  | f0 - 0.17.3+     | f1 - 1.10.x      | f2 - 2.2.x      |
+|------------|------------------|------------------|-----------------|
+| r1_9       | 🤡 f0-r1_9       | ❌                | ❌               |
+| r2_0       | 🤡 f0-r2_0       | ❌                | ❌               |
+| r2_1       | 🤡 f0-r2_1       | ❌                | ❌               |
+| r2_2       | 🤡 f0-r2_2       | ❌                | ❌               |
+| r2_3       | 🚧 f0-r2_3       | ❌                | ❌               |
+| r2_4       | 🚧 f0-r2_4       | 🚧 f1-r2_4       | ❌               |
+| r2_5       | 🚧 f0-r2_5       | 🚧 f1-r2_5       | ❌               |
+| r2_6       | 🚧 f0-r2_6       | 🚧 f1-r2_6       | 🚧 f2-r2_6      |
+| r2_7       | ✅ f0-r2_7        | ✅ f1-r2_7        | ✅ f2-r2_7       |
+| r3_0       | ✅ f0-r3_0        | ✅ f1-r3_0        | ✅ f2-r3_0       |
+| r3_1       | ✅ f0-r3_1        | ✅ f1-r3_1        | ✅ f2-r3_1       |
+| rH3_2      | 🚧 f0-rH3_2      | 🚧 f1-rH3_2      | 🚧 f2-rH3_2     |
+| rmin       | 🙈 f0-rmin       | 🙈 f1-rmin       | 🙈 f2-rmin      |
+| rMS        | 🙈 f0-rMS        | 🙈 f1-rMS        | 🙈 f2-rMS       |
+| jr9_1-r2_3 | 🚧 f0-jr9_1-r2_3 | ❌                | ❌               |
+| jr9_2-r2_5 | 🚧 f0-jr9_2-r2_5 | 🚧 f1-jr9_2-r2_5 | ❌               |
+| jr9_3-r2_6 | ✅ f0-jr9_3-r2_6  | ✅ f1-jr9_3-r2_6  | ✅ f2-jr9_3-r2_6 |
+| jrH-r2_7   | 🚧 f0-jrH-r2_7    | 🚧 f1-jrH-r2_7    | 🚧 f2-jrH-r2_7   |
+| tr19-r2_5  | 🚧 f0-tr19-r2_5  | 🚧 f1-tr19-r2_5  | ❌               |
+| tr20-r2_6  | 🚧 f0-tr20-r2_6  | 🚧 f1-tr20-r2_6  | 🚧 f2-tr20-r2_6 |
+| tr21-r2_7  | ✅ f0-tr21-r2_7   | ✅ f1-tr21-r2_7   | ✅ f2-tr21-r2_7  |
+| tr22-r3_0  | ✅ f0-tr22-r3_0   | ✅ f1-tr22-r3_0   | ✅ f2-tr22-r3_0  |
+| trH-r3_1   | 🚧 f0-trH-r3_1   | 🚧 f1-trH-r3_1   | 🚧 f2-trH-r3_1  |
+| trg21-r2_7 | ✅ f0-trg21-r2_7  | ✅ f1-trg21-r2_7  | ✅ f2-trg21-r2_7 |
+| trg22-r3_0 | ✅ f0-trg22-r3_0  | ✅ f1-trg22-r3_0  | ✅ f2-trg22-r3_0 |
+| trgH-r3_1  | 🚧 f0-trgH-r3_1  | 🚧 f1-trgH-r3_1  | 🚧 f2-trgH-r3_1 |
diff --git a/gemfiles/f0.gemfile b/gemfiles/f0.gemfile
new file mode 100644
index 0000000..67b7e01
--- /dev/null
+++ b/gemfiles/f0.gemfile
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+source 'https://rubygems.org'
+
+# See README.md in this directory
+
+# 0.17.3 is the first version that stops using &Proc.new for block forwarding,
+#   and thus is the oldest version oauth2 is compatible with.
+gem 'faraday', '~> 0.17.4'
+
+gemspec :path => '../'
diff --git a/gemfiles/f1.gemfile b/gemfiles/f1.gemfile
new file mode 100644
index 0000000..f048505
--- /dev/null
+++ b/gemfiles/f1.gemfile
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+source 'https://rubygems.org'
+
+# See README.md in this directory
+
+gem 'faraday', '~> 1.10'
+
+gemspec :path => '../'
diff --git a/gemfiles/f2.gemfile b/gemfiles/f2.gemfile
new file mode 100644
index 0000000..4724ca5
--- /dev/null
+++ b/gemfiles/f2.gemfile
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+source 'https://rubygems.org'
+
+# See README.md in this directory
+
+gem 'faraday', '~> 2.2'
+
+gemspec :path => '../'
diff --git a/gemfiles/jruby_1.7.gemfile b/gemfiles/jruby_1.7.gemfile
index bbef752..5d23c8b 100644
--- a/gemfiles/jruby_1.7.gemfile
+++ b/gemfiles/jruby_1.7.gemfile
@@ -1,10 +1,12 @@
+# frozen_string_literal: true
+
 source 'https://rubygems.org'
 
 gem 'faraday', '~> 0.15.4'
 
 gem 'json', '< 2.0'
 gem 'rack', '~> 1.2'
-gem 'rake', [">= 10.0", "< 12"]
+gem 'rake', ['>= 10.0', '< 12']
 gem 'term-ansicolor', '< 1.4.0'
 gem 'tins', '< 1.7'
 
diff --git a/gemfiles/jruby_9.0.gemfile b/gemfiles/jruby_9.0.gemfile
index 13fd08d..1a2afca 100644
--- a/gemfiles/jruby_9.0.gemfile
+++ b/gemfiles/jruby_9.0.gemfile
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
 source 'https://rubygems.org'
 
 gem 'faraday', '~> 0.15.4'
 
-gem 'rake', [">= 10.0", "< 12"]
+gem 'rake', ['>= 10.0', '< 12']
 
 gemspec :path => '../'
diff --git a/gemfiles/jruby_9.1.gemfile b/gemfiles/jruby_9.1.gemfile
index a02c547..c777028 100644
--- a/gemfiles/jruby_9.1.gemfile
+++ b/gemfiles/jruby_9.1.gemfile
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 source 'https://rubygems.org'
 
 gemspec :path => '../'
diff --git a/gemfiles/jruby_9.2.gemfile b/gemfiles/jruby_9.2.gemfile
index a02c547..c777028 100644
--- a/gemfiles/jruby_9.2.gemfile
+++ b/gemfiles/jruby_9.2.gemfile
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 source 'https://rubygems.org'
 
 gemspec :path => '../'
diff --git a/gemfiles/jruby_head.gemfile b/gemfiles/jruby_head.gemfile
index a02c547..c777028 100644
--- a/gemfiles/jruby_head.gemfile
+++ b/gemfiles/jruby_head.gemfile
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 source 'https://rubygems.org'
 
 gemspec :path => '../'
diff --git a/gemfiles/ruby_1.9.gemfile b/gemfiles/ruby_1.9.gemfile
index bbef752..5d23c8b 100644
--- a/gemfiles/ruby_1.9.gemfile
+++ b/gemfiles/ruby_1.9.gemfile
@@ -1,10 +1,12 @@
+# frozen_string_literal: true
+
 source 'https://rubygems.org'
 
 gem 'faraday', '~> 0.15.4'
 
 gem 'json', '< 2.0'
 gem 'rack', '~> 1.2'
-gem 'rake', [">= 10.0", "< 12"]
+gem 'rake', ['>= 10.0', '< 12']
 gem 'term-ansicolor', '< 1.4.0'
 gem 'tins', '< 1.7'
 
diff --git a/gemfiles/ruby_2.0.gemfile b/gemfiles/ruby_2.0.gemfile
index 87a679f..cc675c4 100644
--- a/gemfiles/ruby_2.0.gemfile
+++ b/gemfiles/ruby_2.0.gemfile
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 source 'https://rubygems.org'
 
 gem 'faraday', '~> 0.15.4'
diff --git a/gemfiles/ruby_2.1.gemfile b/gemfiles/ruby_2.1.gemfile
deleted file mode 100644
index 87a679f..0000000
--- a/gemfiles/ruby_2.1.gemfile
+++ /dev/null
@@ -1,6 +0,0 @@
-source 'https://rubygems.org'
-
-gem 'faraday', '~> 0.15.4'
-gem 'rack', '~> 1.2'
-
-gemspec :path => '../'
diff --git a/gemfiles/ruby_2.2.gemfile b/gemfiles/ruby_2.2.gemfile
deleted file mode 100644
index a02c547..0000000
--- a/gemfiles/ruby_2.2.gemfile
+++ /dev/null
@@ -1,3 +0,0 @@
-source 'https://rubygems.org'
-
-gemspec :path => '../'
diff --git a/gemfiles/ruby_2.3.gemfile b/gemfiles/ruby_2.3.gemfile
deleted file mode 100644
index a02c547..0000000
--- a/gemfiles/ruby_2.3.gemfile
+++ /dev/null
@@ -1,3 +0,0 @@
-source 'https://rubygems.org'
-
-gemspec :path => '../'
diff --git a/gemfiles/ruby_2.4.gemfile b/gemfiles/ruby_2.4.gemfile
deleted file mode 100644
index a02c547..0000000
--- a/gemfiles/ruby_2.4.gemfile
+++ /dev/null
@@ -1,3 +0,0 @@
-source 'https://rubygems.org'
-
-gemspec :path => '../'
diff --git a/gemfiles/ruby_2.5.gemfile b/gemfiles/ruby_2.5.gemfile
deleted file mode 100644
index a02c547..0000000
--- a/gemfiles/ruby_2.5.gemfile
+++ /dev/null
@@ -1,3 +0,0 @@
-source 'https://rubygems.org'
-
-gemspec :path => '../'
diff --git a/gemfiles/ruby_2.6.gemfile b/gemfiles/ruby_2.6.gemfile
deleted file mode 100644
index 822e2f2..0000000
--- a/gemfiles/ruby_2.6.gemfile
+++ /dev/null
@@ -1,9 +0,0 @@
-source 'https://rubygems.org'
-
-group :development do
-  gem 'pry'
-  gem 'byebug'
-  gem 'pry-byebug'
-end
-
-gemspec :path => '../'
diff --git a/gemfiles/ruby_2.7.gemfile b/gemfiles/ruby_2.7.gemfile
deleted file mode 100644
index 822e2f2..0000000
--- a/gemfiles/ruby_2.7.gemfile
+++ /dev/null
@@ -1,9 +0,0 @@
-source 'https://rubygems.org'
-
-group :development do
-  gem 'pry'
-  gem 'byebug'
-  gem 'pry-byebug'
-end
-
-gemspec :path => '../'
diff --git a/gemfiles/ruby_head.gemfile b/gemfiles/ruby_head.gemfile
index 822e2f2..03b6a53 100644
--- a/gemfiles/ruby_head.gemfile
+++ b/gemfiles/ruby_head.gemfile
@@ -1,8 +1,10 @@
+# frozen_string_literal: true
+
 source 'https://rubygems.org'
 
 group :development do
-  gem 'pry'
   gem 'byebug'
+  gem 'pry'
   gem 'pry-byebug'
 end
 
diff --git a/gemfiles/truffleruby.gemfile b/gemfiles/truffleruby.gemfile
index a02c547..c777028 100644
--- a/gemfiles/truffleruby.gemfile
+++ b/gemfiles/truffleruby.gemfile
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 source 'https://rubygems.org'
 
 gemspec :path => '../'
diff --git a/lib/oauth2.rb b/lib/oauth2.rb
index 5a7ed9f..77a93f5 100644
--- a/lib/oauth2.rb
+++ b/lib/oauth2.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'oauth2/error'
 require 'oauth2/authenticator'
 require 'oauth2/client'
diff --git a/lib/oauth2/access_token.rb b/lib/oauth2/access_token.rb
index 2156753..c656804 100644
--- a/lib/oauth2/access_token.rb
+++ b/lib/oauth2/access_token.rb
@@ -1,8 +1,11 @@
+# frozen_string_literal: true
+
 module OAuth2
   class AccessToken
     attr_reader :client, :token, :expires_in, :expires_at, :params
     attr_accessor :options, :refresh_token
 
+    # Should these methods be deprecated?
     class << self
       # Initializes an AccessToken from a Hash
       #
@@ -37,7 +40,7 @@ module OAuth2
     # @option opts [String] :header_format ('Bearer %s') the string format to use for the Authorization header
     # @option opts [String] :param_name ('access_token') the parameter name to use for transmission of the
     #    Access Token value in :body or :query transmission mode
-    def initialize(client, token, opts = {}) # rubocop:disable Metrics/AbcSize
+    def initialize(client, token, opts = {})
       @client = client
       @token = token.to_s
       opts = opts.dup
@@ -48,9 +51,9 @@ module OAuth2
       @expires_in &&= @expires_in.to_i
       @expires_at &&= convert_expires_at(@expires_at)
       @expires_at ||= Time.now.to_i + @expires_in if @expires_in
-      @options = {:mode          => opts.delete(:mode) || :header,
+      @options = {:mode => opts.delete(:mode) || :header,
                   :header_format => opts.delete(:header_format) || 'Bearer %s',
-                  :param_name    => opts.delete(:param_name) || 'access_token'}
+                  :param_name => opts.delete(:param_name) || 'access_token'}
       @params = opts
     end
 
@@ -81,6 +84,7 @@ module OAuth2
     # @note options should be carried over to the new AccessToken
     def refresh!(params = {})
       raise('A refresh_token is not available') unless refresh_token
+
       params[:grant_type] = 'refresh_token'
       params[:refresh_token] = refresh_token
       new_token = @client.get_token(params)
@@ -149,7 +153,7 @@ module OAuth2
 
   private
 
-    def configure_authentication!(opts) # rubocop:disable MethodLength, Metrics/AbcSize
+    def configure_authentication!(opts)
       case options[:mode]
       when :header
         opts[:headers] ||= {}
@@ -171,10 +175,9 @@ module OAuth2
     end
 
     def convert_expires_at(expires_at)
-      expires_at_i = expires_at.to_i
-      return expires_at_i if expires_at_i > Time.now.utc.to_i
-      return Time.parse(expires_at).to_i if expires_at.is_a?(String)
-      expires_at_i
+      Time.iso8601(expires_at.to_s).to_i
+    rescue ArgumentError
+      expires_at.to_i
     end
   end
 end
diff --git a/lib/oauth2/authenticator.rb b/lib/oauth2/authenticator.rb
index 6194b83..34b696a 100644
--- a/lib/oauth2/authenticator.rb
+++ b/lib/oauth2/authenticator.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'base64'
 
 module OAuth2
@@ -49,7 +51,7 @@ module OAuth2
     # When using schemes that don't require the client_secret to be passed i.e TLS Client Auth,
     # we don't want to send the secret
     def apply_client_id(params)
-      { 'client_id' => id }.merge(params)
+      {'client_id' => id}.merge(params)
     end
 
     # Adds an `Authorization` header with Basic Auth credentials if and only if
@@ -60,7 +62,7 @@ module OAuth2
       params.merge(:headers => headers)
     end
 
-    # @see https://tools.ietf.org/html/rfc2617#section-2
+    # @see https://datatracker.ietf.org/doc/html/rfc2617#section-2
     def basic_auth_header
       {'Authorization' => self.class.encode_basic_auth(id, secret)}
     end
diff --git a/lib/oauth2/client.rb b/lib/oauth2/client.rb
index b378022..611dab2 100644
--- a/lib/oauth2/client.rb
+++ b/lib/oauth2/client.rb
@@ -1,9 +1,14 @@
+# frozen_string_literal: true
+
 require 'faraday'
 require 'logger'
 
 module OAuth2
+  ConnectionError = Class.new(Faraday::ConnectionFailed)
   # The OAuth2::Client class
   class Client # rubocop:disable Metrics/ClassLength
+    RESERVED_PARAM_KEYS = %w[headers parse].freeze
+
     attr_reader :id, :secret, :site
     attr_accessor :options
     attr_writer :connection
@@ -14,17 +19,18 @@ module OAuth2
     #
     # @param [String] client_id the client_id value
     # @param [String] client_secret the client_secret value
-    # @param [Hash] opts the options to create the client with
-    # @option opts [String] :site the OAuth2 provider site host
-    # @option opts [String] :redirect_uri the absolute URI to the Redirection Endpoint for use in authorization grants and token exchange
-    # @option opts [String] :authorize_url ('/oauth/authorize') absolute or relative URL path to the Authorization endpoint
-    # @option opts [String] :token_url ('/oauth/token') absolute or relative URL path to the Token endpoint
-    # @option opts [Symbol] :token_method (:post) HTTP method to use to request token (:get or :post)
-    # @option opts [Symbol] :auth_scheme (:basic_auth) HTTP method to use to authorize request (:basic_auth or :request_body)
-    # @option opts [Hash] :connection_opts ({}) Hash of connection options to pass to initialize Faraday with
-    # @option opts [FixNum] :max_redirects (5) maximum number of redirects to follow
-    # @option opts [Boolean] :raise_errors (true) whether or not to raise an OAuth2::Error
-    #  on responses with 400+ status codes
+    # @param [Hash] options the options to create the client with
+    # @option options [String] :site the OAuth2 provider site host
+    # @option options [String] :redirect_uri the absolute URI to the Redirection Endpoint for use in authorization grants and token exchange
+    # @option options [String] :authorize_url ('oauth/authorize') absolute or relative URL path to the Authorization endpoint
+    # @option options [String] :token_url ('oauth/token') absolute or relative URL path to the Token endpoint
+    # @option options [Symbol] :token_method (:post) HTTP method to use to request token (:get or :post)
+    # @option options [Symbol] :auth_scheme (:basic_auth) HTTP method to use to authorize request (:basic_auth or :request_body)
+    # @option options [Hash] :connection_opts ({}) Hash of connection options to pass to initialize Faraday with
+    # @option options [FixNum] :max_redirects (5) maximum number of redirects to follow
+    # @option options [Boolean] :raise_errors (true) whether or not to raise an OAuth2::Error on responses with 400+ status codes
+    # @option options [Logger] :logger (::Logger.new($stdout)) which logger to use when OAUTH_DEBUG is enabled
+    # @option options [Proc] (DEPRECATED) :extract_access_token  proc that extracts the access token from the response
     # @yield [builder] The Faraday connection builder
     def initialize(client_id, client_secret, options = {}, &block)
       opts = options.dup
@@ -32,20 +38,22 @@ module OAuth2
       @secret = client_secret
       @site = opts.delete(:site)
       ssl = opts.delete(:ssl)
-      @options = {:authorize_url    => '/oauth/authorize',
-                  :token_url        => '/oauth/token',
-                  :token_method     => :post,
-                  :auth_scheme      => :request_body,
-                  :connection_opts  => {},
+      @options = {:authorize_url => 'oauth/authorize',
+                  :token_url => 'oauth/token',
+                  :token_method => :post,
+                  :auth_scheme => :request_body,
+                  :connection_opts => {},
                   :connection_build => block,
-                  :max_redirects    => 5,
-                  :raise_errors     => true}.merge(opts)
+                  :max_redirects => 5,
+                  :raise_errors => true,
+                  :extract_access_token => DEFAULT_EXTRACT_ACCESS_TOKEN, # DEPRECATED
+                  :logger => ::Logger.new($stdout)}.merge(opts)
       @options[:connection_opts][:ssl] = ssl if ssl
     end
 
     # Set the site host
     #
-    # @param [String] the OAuth2 provider site host
+    # @param value [String] the OAuth2 provider site host
     def site=(value)
       @connection = nil
       @site = value
@@ -53,15 +61,16 @@ module OAuth2
 
     # The Faraday connection object
     def connection
-      @connection ||= begin
-        conn = Faraday.new(site, options[:connection_opts])
-        if options[:connection_build]
-          conn.build do |b|
-            options[:connection_build].call(b)
+      @connection ||=
+        Faraday.new(site, options[:connection_opts]) do |builder|
+          oauth_debug_logging(builder)
+          if options[:connection_build]
+            options[:connection_build].call(builder)
+          else
+            builder.request :url_encoded             # form-encode POST params
+            builder.adapter Faraday.default_adapter  # make requests with Net::HTTP
           end
         end
-        conn
-      end
     end
 
     # The authorize endpoint URL of the OAuth2 provider
@@ -91,15 +100,18 @@ module OAuth2
     #   code response for this request.  Will default to client option
     # @option opts [Symbol] :parse @see Response::initialize
     # @yield [req] The Faraday request
-    def request(verb, url, opts = {}) # rubocop:disable CyclomaticComplexity, MethodLength, Metrics/AbcSize
-      connection.response :logger, ::Logger.new($stdout) if ENV['OAUTH_DEBUG'] == 'true'
-
+    def request(verb, url, opts = {}) # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/AbcSize
       url = connection.build_url(url).to_s
 
-      response = connection.run_request(verb, url, opts[:body], opts[:headers]) do |req|
-        req.params.update(opts[:params]) if opts[:params]
-        yield(req) if block_given?
+      begin
+        response = connection.run_request(verb, url, opts[:body], opts[:headers]) do |req|
+          req.params.update(opts[:params]) if opts[:params]
+          yield(req) if block_given?
+        end
+      rescue Faraday::ConnectionFailed => e
+        raise ConnectionError, e
       end
+
       response = Response.new(response, :parse => opts[:parse])
 
       case response.status
@@ -107,17 +119,25 @@ module OAuth2
         opts[:redirect_count] ||= 0
         opts[:redirect_count] += 1
         return response if opts[:redirect_count] > options[:max_redirects]
+
         if response.status == 303
           verb = :get
           opts.delete(:body)
         end
-        request(verb, response.headers['location'], opts)
+        location = response.headers['location']
+        if location
+          request(verb, location, opts)
+        else
+          error = Error.new(response)
+          raise(error, "Got #{response.status} status code, but no Location header was present")
+        end
       when 200..299, 300..399
         # on non-redirecting 3xx statuses, just return the response
         response
       when 400..599
         error = Error.new(response)
         raise(error) if opts.fetch(:raise_errors, options[:raise_errors])
+
         response.error = error
         response
       else
@@ -128,12 +148,21 @@ module OAuth2
 
     # Initializes an AccessToken by making a request to the token endpoint
     #
-    # @param [Hash] params a Hash of params for the token endpoint
-    # @param [Hash] access token options, to pass to the AccessToken object
-    # @param [Class] class of access token for easier subclassing OAuth2::AccessToken
+    # @param params [Hash] a Hash of params for the token endpoint
+    # @param access_token_opts [Hash] access token options, to pass to the AccessToken object
+    # @param access_token_class [Class] class of access token for easier subclassing OAuth2::AccessToken
     # @return [AccessToken] the initialized AccessToken
-    def get_token(params, access_token_opts = {}, access_token_class = AccessToken) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
-      params = Authenticator.new(id, secret, options[:auth_scheme]).apply(params)
+    def get_token(params, access_token_opts = {}, extract_access_token = options[:extract_access_token]) # # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity Metrics/AbcSize, Metrics/MethodLength
+      params = params.map do |key, value|
+        if RESERVED_PARAM_KEYS.include?(key)
+          [key.to_sym, value]
+        else
+          [key, value]
+        end
+      end
+      params = Hash[params]
+
+      params = authenticator.apply(params)
       opts = {:raise_errors => options[:raise_errors], :parse => params.delete(:parse)}
       headers = params.delete(:headers) || {}
       if options[:token_method] == :post
@@ -143,39 +172,55 @@ module OAuth2
         opts[:params] = params
         opts[:headers] = {}
       end
-      opts[:headers].merge!(headers)
-      response = request(options[:token_method], token_url, opts)
-      if options[:raise_errors] && !(response.parsed.is_a?(Hash) && response.parsed['access_token'])
+      opts[:headers] = opts[:headers].merge(headers)
+      http_method = options[:token_method]
+      response = request(http_method, token_url, opts)
+
+      access_token = begin
+        build_access_token(response, access_token_opts, extract_access_token)
+      rescue StandardError
+        nil
+      end
+
+      response_contains_token = access_token || (
+                                  response.parsed.is_a?(Hash) &&
+                                  (response.parsed['access_token'] || response.parsed['id_token'])
+                                )
+
+      if options[:raise_errors] && !response_contains_token
         error = Error.new(response)
         raise(error)
+      elsif !response_contains_token
+        return nil
       end
-      access_token_class.from_hash(self, response.parsed.merge(access_token_opts))
+
+      access_token
     end
 
     # The Authorization Code strategy
     #
-    # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.1
+    # @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-15#section-4.1
     def auth_code
       @auth_code ||= OAuth2::Strategy::AuthCode.new(self)
     end
 
     # The Implicit strategy
     #
-    # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-26#section-4.2
+    # @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-26#section-4.2
     def implicit
       @implicit ||= OAuth2::Strategy::Implicit.new(self)
     end
 
     # The Resource Owner Password Credentials strategy
     #
-    # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.3
+    # @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-15#section-4.3
     def password
       @password ||= OAuth2::Strategy::Password.new(self)
     end
 
     # The Client Credentials strategy
     #
-    # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.4
+    # @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-15#section-4.4
     def client_credentials
       @client_credentials ||= OAuth2::Strategy::ClientCredentials.new(self)
     end
@@ -195,10 +240,10 @@ module OAuth2
     #
     # @api semipublic
     #
-    # @see https://tools.ietf.org/html/rfc6749#section-4.1
-    # @see https://tools.ietf.org/html/rfc6749#section-4.1.3
-    # @see https://tools.ietf.org/html/rfc6749#section-4.2.1
-    # @see https://tools.ietf.org/html/rfc6749#section-10.6
+    # @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1
+    # @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3
+    # @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.1
+    # @see https://datatracker.ietf.org/doc/html/rfc6749#section-10.6
     # @return [Hash] the params to add to a request or URL
     def redirection_params
       if options[:redirect_uri]
@@ -207,5 +252,41 @@ module OAuth2
         {}
       end
     end
+
+    DEFAULT_EXTRACT_ACCESS_TOKEN = proc do |client, hash|
+      token = hash.delete('access_token') || hash.delete(:access_token)
+      token && AccessToken.new(client, token, hash)
+    end
+
+  private
+
+    # Returns the authenticator object
+    #
+    # @return [Authenticator] the initialized Authenticator
+    def authenticator
+      Authenticator.new(id, secret, options[:auth_scheme])
+    end
+
+    # Builds the access token from the response of the HTTP call
+    #
+    # @return [AccessToken] the initialized AccessToken
+    def build_access_token(response, access_token_opts, extract_access_token)
+      parsed_response = response.parsed.dup
+      return unless parsed_response.is_a?(Hash)
+
+      hash = parsed_response.merge(access_token_opts)
+
+      # Provide backwards compatibility for old AccessToken.form_hash pattern
+      # Will be deprecated in 2.x
+      if extract_access_token.is_a?(Class) && extract_access_token.respond_to?(:from_hash)
+        extract_access_token.from_hash(self, hash)
+      else
+        extract_access_token.call(self, hash)
+      end
+    end
+
+    def oauth_debug_logging(builder)
+      builder.response :logger, options[:logger], :bodies => true if ENV['OAUTH_DEBUG'] == 'true'
+    end
   end
 end
diff --git a/lib/oauth2/error.rb b/lib/oauth2/error.rb
index c2618a1..64f6329 100644
--- a/lib/oauth2/error.rb
+++ b/lib/oauth2/error.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 module OAuth2
   class Error < StandardError
     attr_reader :response, :code, :description
@@ -23,7 +25,7 @@ module OAuth2
     def error_message(response_body, opts = {})
       message = []
 
-      opts[:error_description] && message << opts[:error_description]
+      opts[:error_description] && (message << opts[:error_description])
 
       error_message = if opts[:error_description] && opts[:error_description].respond_to?(:encoding)
                         script_encoding = opts[:error_description].encoding
diff --git a/lib/oauth2/mac_token.rb b/lib/oauth2/mac_token.rb
index db7d4d7..69c5ade 100644
--- a/lib/oauth2/mac_token.rb
+++ b/lib/oauth2/mac_token.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'base64'
 require 'digest'
 require 'openssl'
@@ -95,16 +97,22 @@ module OAuth2
     #
     # @param [String] alg the algorithm to use (one of 'hmac-sha-1', 'hmac-sha-256')
     def algorithm=(alg)
-      @algorithm = begin
-        case alg.to_s
-        when 'hmac-sha-1'
-          OpenSSL::Digest::SHA1.new
-        when 'hmac-sha-256'
-          OpenSSL::Digest::SHA256.new
-        else
-          raise(ArgumentError, 'Unsupported algorithm')
-        end
-      end
+      @algorithm = case alg.to_s
+                   when 'hmac-sha-1'
+                     begin
+                       OpenSSL::Digest('SHA1').new
+                     rescue StandardError
+                       OpenSSL::Digest.new('SHA1')
+                     end
+                   when 'hmac-sha-256'
+                     begin
+                       OpenSSL::Digest('SHA256').new
+                     rescue StandardError
+                       OpenSSL::Digest.new('SHA256')
+                     end
+                   else
+                     raise(ArgumentError, 'Unsupported algorithm')
+                   end
     end
 
   private
diff --git a/lib/oauth2/response.rb b/lib/oauth2/response.rb
index 13657fd..9a4f5d5 100644
--- a/lib/oauth2/response.rb
+++ b/lib/oauth2/response.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 require 'multi_json'
 require 'multi_xml'
 require 'rack'
@@ -11,9 +13,9 @@ module OAuth2
     # Procs that, when called, will parse a response body according
     # to the specified format.
     @@parsers = {
-      :json  => lambda { |body| MultiJson.load(body) rescue body }, # rubocop:disable RescueModifier
+      :json => lambda { |body| MultiJson.load(body) rescue body }, # rubocop:disable Style/RescueModifier
       :query => lambda { |body| Rack::Utils.parse_query(body) },
-      :text  => lambda { |body| body },
+      :text => lambda { |body| body },
     }
 
     # Content type assignments for various potential HTTP content types.
@@ -68,6 +70,7 @@ module OAuth2
     #   application/json Content-Type response bodies
     def parsed
       return nil unless @@parsers.key?(parser)
+
       @parsed ||= @@parsers[parser].call(body)
     end
 
@@ -79,11 +82,12 @@ module OAuth2
     # Determines the parser that will be used to supply the content of #parsed
     def parser
       return options[:parse].to_sym if @@parsers.key?(options[:parse])
+
       @@content_types[content_type]
     end
   end
 end
 
 OAuth2::Response.register_parser(:xml, ['text/xml', 'application/rss+xml', 'application/rdf+xml', 'application/atom+xml']) do |body|
-  MultiXml.parse(body) rescue body # rubocop:disable RescueModifier
+  MultiXml.parse(body) rescue body # rubocop:disable Style/RescueModifier
 end
diff --git a/lib/oauth2/strategy/assertion.rb b/lib/oauth2/strategy/assertion.rb
index b3b577b..2f3351a 100644
--- a/lib/oauth2/strategy/assertion.rb
+++ b/lib/oauth2/strategy/assertion.rb
@@ -1,10 +1,12 @@
+# frozen_string_literal: true
+
 require 'jwt'
 
 module OAuth2
   module Strategy
     # The Client Assertion Strategy
     #
-    # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-10#section-4.1.3
+    # @see https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-10#section-4.1.3
     #
     # Sample usage:
     #   client = OAuth2::Client.new(client_id, client_secret,
@@ -50,10 +52,10 @@ module OAuth2
       def build_request(params)
         assertion = build_assertion(params)
         {
-          :grant_type     => 'assertion',
+          :grant_type => 'assertion',
           :assertion_type => 'urn:ietf:params:oauth:grant-type:jwt-bearer',
-          :assertion      => assertion,
-          :scope          => params[:scope],
+          :assertion => assertion,
+          :scope => params[:scope],
         }
       end
 
diff --git a/lib/oauth2/strategy/auth_code.rb b/lib/oauth2/strategy/auth_code.rb
index 76f92aa..b019ad9 100644
--- a/lib/oauth2/strategy/auth_code.rb
+++ b/lib/oauth2/strategy/auth_code.rb
@@ -1,8 +1,10 @@
+# frozen_string_literal: true
+
 module OAuth2
   module Strategy
     # The Authorization Code Strategy
     #
-    # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.1
+    # @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-15#section-4.1
     class AuthCode < Base
       # The required query parameters for the authorize URL
       #
diff --git a/lib/oauth2/strategy/base.rb b/lib/oauth2/strategy/base.rb
index 9d16bb4..801a723 100644
--- a/lib/oauth2/strategy/base.rb
+++ b/lib/oauth2/strategy/base.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
 module OAuth2
   module Strategy
     class Base
diff --git a/lib/oauth2/strategy/client_credentials.rb b/lib/oauth2/strategy/client_credentials.rb
index 35ac5fd..778c4fa 100644
--- a/lib/oauth2/strategy/client_credentials.rb
+++ b/lib/oauth2/strategy/client_credentials.rb
@@ -1,8 +1,10 @@
+# frozen_string_literal: true
+
 module OAuth2
   module Strategy
     # The Client Credentials Strategy
     #
-    # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.4
+    # @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-15#section-4.4
     class ClientCredentials < Base
       # Not used for this strategy
       #
diff --git a/lib/oauth2/strategy/implicit.rb b/lib/oauth2/strategy/implicit.rb
index 6ab505d..00d3e4e 100644
--- a/lib/oauth2/strategy/implicit.rb
+++ b/lib/oauth2/strategy/implicit.rb
@@ -1,8 +1,10 @@
+# frozen_string_literal: true
+
 module OAuth2
   module Strategy
     # The Implicit Strategy
     #
-    # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-26#section-4.2
+    # @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-26#section-4.2
     class Implicit < Base
       # The required query parameters for the authorize URL
       #
diff --git a/lib/oauth2/strategy/password.rb b/lib/oauth2/strategy/password.rb
index 49bfc6e..d41ca07 100644
--- a/lib/oauth2/strategy/password.rb
+++ b/lib/oauth2/strategy/password.rb
@@ -1,8 +1,10 @@
+# frozen_string_literal: true
+
 module OAuth2
   module Strategy
     # The Resource Owner Password Credentials Authorization Strategy
     #
-    # @see http://tools.ietf.org/html/draft-ietf-oauth-v2-15#section-4.3
+    # @see http://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-15#section-4.3
     class Password < Base
       # Not used for this strategy
       #
@@ -18,8 +20,8 @@ module OAuth2
       # @param [Hash] params additional params
       def get_token(username, password, params = {}, opts = {})
         params = {'grant_type' => 'password',
-                  'username'   => username,
-                  'password'   => password}.merge(params)
+                  'username' => username,
+                  'password' => password}.merge(params)
         @client.get_token(params, opts)
       end
     end
diff --git a/lib/oauth2/version.rb b/lib/oauth2/version.rb
index 684cdcf..88cf98d 100644
--- a/lib/oauth2/version.rb
+++ b/lib/oauth2/version.rb
@@ -1,5 +1,9 @@
+# frozen_string_literal: true
+
 module OAuth2
   module Version
+    VERSION = to_s
+
   module_function
 
     # The major version
@@ -20,12 +24,12 @@ module OAuth2
     #
     # @return [Integer]
     def patch
-      4
+      9
     end
 
     # The pre-release version, if any
     #
-    # @return [Integer, NilClass]
+    # @return [String, NilClass]
     def pre
       nil
     end
@@ -53,7 +57,9 @@ module OAuth2
     #
     # @return [String]
     def to_s
-      to_a.join('.')
+      v = [major, minor, patch].compact.join('.')
+      v += "-#{pre}" if pre
+      v
     end
   end
 end
diff --git a/maintenance-branch b/maintenance-branch
new file mode 100644
index 0000000..8b25206
--- /dev/null
+++ b/maintenance-branch
@@ -0,0 +1 @@
+master
\ No newline at end of file
diff --git a/oauth2.gemspec b/oauth2.gemspec
index 1e8b8aa..b6ee8cc 100644
--- a/oauth2.gemspec
+++ b/oauth2.gemspec
@@ -1,11 +1,12 @@
 # coding: utf-8
+# frozen_string_literal: true
 
 lib = File.expand_path('../lib', __FILE__)
 $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
 require 'oauth2/version'
 
 Gem::Specification.new do |spec|
-  spec.add_dependency 'faraday', ['>= 0.8', '< 2.0']
+  spec.add_dependency 'faraday', ['>= 0.17.3', '< 3.0']
   spec.add_dependency 'jwt', ['>= 1.0', '< 3.0']
   spec.add_dependency 'multi_json', '~> 1.3'
   spec.add_dependency 'multi_xml', '~> 0.5'
@@ -20,33 +21,30 @@ Gem::Specification.new do |spec|
   spec.required_ruby_version = '>= 1.9.0'
   spec.required_rubygems_version = '>= 1.3.5'
   spec.summary       = 'A Ruby wrapper for the OAuth 2.0 protocol.'
-  spec.version       = OAuth2::Version
+  spec.version       = OAuth2::Version.to_s
 
   spec.metadata = {
-    'bug_tracker_uri'   => 'https://github.com/oauth-xx/oauth2/issues',
-    'changelog_uri'     => "https://github.com/oauth-xx/oauth2/blob/v#{spec.version}/CHANGELOG.md",
+    'bug_tracker_uri' => 'https://github.com/oauth-xx/oauth2/issues',
+    'changelog_uri' => "https://github.com/oauth-xx/oauth2/blob/v#{spec.version}/CHANGELOG.md",
     'documentation_uri' => "https://www.rubydoc.info/gems/oauth2/#{spec.version}",
-    'source_code_uri'   => "https://github.com/oauth-xx/oauth2/tree/v#{spec.version}",
-    'wiki_uri'          => 'https://github.com/oauth-xx/oauth2/wiki'
+    'source_code_uri' => "https://github.com/oauth-xx/oauth2/tree/v#{spec.version}",
+    'wiki_uri' => 'https://github.com/oauth-xx/oauth2/wiki',
+    'rubygems_mfa_required' => 'true',
   }
 
   spec.require_paths = %w[lib]
   spec.bindir        = 'exe'
-  spec.executables   = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
-  spec.files         = `git ls-files -z`.split("\x0").reject do |f|
-    f.match(%r{^(bin|test|spec|features)/})
-  end
+  spec.files = Dir['lib/**/*', 'LICENSE', 'README.md', 'CHANGELOG.md', 'CODE_OF_CONDUCT.md']
+  spec.test_files = Dir['spec/**/*']
+  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
 
   spec.add_development_dependency 'addressable', '~> 2.3'
-  spec.add_development_dependency 'backports', '~> 3.11'
   spec.add_development_dependency 'bundler', '>= 1.16'
-  spec.add_development_dependency 'coveralls', '~> 0.8'
   spec.add_development_dependency 'rake', '~> 12.3'
-  spec.add_development_dependency 'rdoc', ['>= 5.0', '< 7']
+  spec.add_development_dependency 'rexml', '~> 3.2'
   spec.add_development_dependency 'rspec', '~> 3.0'
-  spec.add_development_dependency 'rspec-stubbed_env'
-  spec.add_development_dependency 'rspec-pending_for'
   spec.add_development_dependency 'rspec-block_is_expected'
+  spec.add_development_dependency 'rspec-pending_for'
+  spec.add_development_dependency 'rspec-stubbed_env'
   spec.add_development_dependency 'silent_stream'
-  spec.add_development_dependency 'wwtd'
 end
diff --git a/spec/fixtures/README.md b/spec/fixtures/README.md
new file mode 100644
index 0000000..e8ed536
--- /dev/null
+++ b/spec/fixtures/README.md
@@ -0,0 +1,11 @@
+# RS256
+
+## How keys were made
+
+```shell
+# No passphrase
+# Generates the public and private keys:
+ssh-keygen -t rsa -b 4096 -m PEM -f jwtRS256.key
+# Converts the key to PEM format
+openssl rsa -in jwtRS256.key -pubout -outform PEM -out jwtRS256.key.pub
+```
diff --git a/spec/fixtures/RS256/jwtRS256.key b/spec/fixtures/RS256/jwtRS256.key
new file mode 100644
index 0000000..72005e5
--- /dev/null
+++ b/spec/fixtures/RS256/jwtRS256.key
@@ -0,0 +1,51 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIJKwIBAAKCAgEA5hdXV/4YSymY1T9VNvK2bWRfulwIty1RnAPNINQmfh3aRRkV
++PNrbC2Crji9G0AHmQwgW1bZ3kgkkpIm6RVn44fHvBvuXkZ9ABgXw0d2cLIHmwOF
+xSKmWAm/EW//GszUTLLLsMZUe2udtFJW0jxXB2GRY0WVYuo6Oo58RCeP719lw3Ag
+s0YF9/IobxKkGd4BautUPw6ZszAa3o+j0zR74x7ouPxybZAOuPsMxqanyeYJeH4o
+sJjLMYV9qem9uG2sj7GENJ8UszcpmGbqxBhexPEB7mgDeONIF0XJF23zdOf8ANE5
+mAU2h2v7M6moAfkdUzJ+j48+VT2omHAzAL5yNcmrl2xiWdyoxOw1Y1UmfEmJYV5V
+gGYyZ12JZRKY+szPT+vR+MDuYxbquF40O7kvkFNBfL1yCpzfSQCLnEs4rX8qRzZX
+ciLeyq4Ht5FLuRFgxjA//XI8LAmp0u7gk+Q7FUH1UgW3kmJDTG0XaxQxYTBSIO7m
+cmyjDyBgKVuQmt5E1ycFeteOVdPD/CG/fPYhthvc4UytEFwsMdNy3iD6/wuUH68t
+AKam28UZaOb0qK+00cQQD8fulY9rKtSL10LvJFWUOa/SJyLvk9vUmfvFn182il1n
+X6GpyxyMmE/FCnH4CT/DjrSZf08mOO8eL5ofYHMK/oiXr1eODqx+pOwClNsCAwEA
+AQKCAgEAy34vMFI4WBk04rx9d/hWoQ7Znu8QgjihaZLvEy6t0HJEfUH/bcqS4fyq
+C72Aeh452gCgiUeZrf4t4jdCFHhrBg8q9dHaEiTTHocwVPPZ6zd4hH8sCrpnVYth
+IWHkw2YOCLtEbFYrl3AI7Na5lHvrGEsREzQSN4Yh83Has0guAy1iyeNb+FFgq/XO
+DtX0ri/rHw1717zo8FIGIXn2EK/lNWw7tIcICKAUdUMK/JGd6XD6RUeGYxDu/CAs
+kF55/Sd6Kyd7XjKnUwzhS7kRvlYzUog4BgqVr4+LTZHZlFAYtfcJqAtinXFW1ZQJ
+eZp9TSlt5wvMZNjx7t92QUNRyEGmrQAU+8COHnT0/drFf0MCiyHSUN0E7/5fswhc
+uMSU9XiJA9G0wYvJl4zIuOuIYWZWhIqvjYSkvdlP70t9XO2gk/ZcCWsMW8i+xbwC
+w1+MMjsKsNedXxI99TIPPHcCNMxqlt1E1kHH3SAwCuEH/ez7PRMyEQQ0EyAk22x/
+piYIWXkX5835cLbLRIYafXgOiugWZjCwIqfRIcIpscmcijZwCF2DyevveYdx3krR
+FGA2PFydFyxCNG7XwvKb9kHb7WBERUPV/H3eCqu2SZ/RvF+I94LUYP4bu6CmFdO9
+wCJcGJoL1P7tVhS9lA5Oj0QWczrjnejCoI9XMMduWk032rR1VYECggEBAPZDnTBY
+H2uiVmGdMfWTAmX86kiHVpkL03OG6rgvDMsMOYKnik9Lb3gNeUIuPeAWFNrXCoD1
+qp0loxPhKSojNOOM8Yiz/GwQ/QI9dzgtxs7E7rFFyTuJcY48Do8uOFyUHbAbeOBF
+b9UL/uBfWZGVV1YY753xyqYlCpxTVQGms1jsbVFdZE1iVpOwAkFVuoLYaHLut4zB
+01ORyBSoWan173P+IQH6F1uNXE2Kk/FIMDN6bgP1pXkdkrTx4WjAmRnP/Sc4r38/
+F1xN+gxnWGPUKDVRPYBpVzDR036w65ODgg2FROK2vIxlStiAC/rc0JLsvaWfb1Rn
+dsWdJJ1V6mZ6a5sCggEBAO8wC1jcIoiBz3xoA8E5BSt8qLJ7ZuSFaaidvWX2/xj6
+lSWJxCGQfhR7P6ozvH6UDo1WbJT6nNyXPkiDkAzcmAdsYVjULW3K2LI9oPajaJxY
+L7KJpylgh9JhMvbMz3VVjTgYRt+kjX+3uFMZNx1YfiBP+S6xx5sjK9CKDz3H99kC
+q9bX95YFqZ7yFE3aBCR6CENo2tXpMN96CLQGpwa0bwt3xNzC4MhZMXbGR3DdBYbD
+tS9lJfQvAVUYxbSE/2FBgjpO6ArMyU2ZUEDFx9J6IhfhVbQV4VeITMyRNo0XwBiQ
+/+XpLXgHkw7LiNMIoc7d+M7yLA1Vz7+r8XxWHHZCL8ECggEBAPK8VrYORno7e1Wg
+MlxS2WxZzTxMWmlkpLoc5END7SI/HHjSV5wtSORWs40uM0MrwMasa+gNPmzDamjv
+6Tllln4ssO8EKe0DGcAZgefYBzxMFNKbbOzIXyvJurga4Ocv/8tUaOL2znJ67nGO
+yqSbRYjR724JpKv7mufXo9SK0gD2mhI3MeSs55WPScnIjJzoXpva/QU7D+gxq7vg
+7PCAP9RfS329W0Sco7yyuXx8oTY8mTBB8ybcpXzBZmNwY/hzcJ42W5XbRFVxbuTH
+APL1beSP/UUTkCPIzuTz0mCGoaxeDjZB1Lu2I/4eyLAu80+/FneoHX5etU23xR1o
+UDFOvb0CggEBALTTc6CoPAtLaBs7X6tSelAYHEli9bTKD8kEB83wX4b42ozYjEh7
+vnWpf8Yi+twO/rlnnws6NCCoztNvcxXmJ6FlFGtdbULV2eFWqjwL6ehY2yZ03sVv
+Tv+DsE3ZJPYlyW+hGuO0uazWrilUpNAwuJmhHFdq2+azPkqYNVGVvhB37oWsHGd0
+vHmHtkXtDris8VZVDSwu8V3iGnZPmTJ+cn0O/OuRAPM2SyjqWdQ/pA/wIShFpd3n
+M3CsG7uP2KokJloCkXaov39E6uEtJRZAc0nudyaAbC4Kw1Tca4tba0SnSm78S/20
+bD8BLN2uZvXH5nQ9rYQfXcIgMZ64UygsfYECggEBAIw0fQaIVmafa0Hz3ipD4PJI
+5QNkh2t9hvOCSKm1xYTNATl0q/VIkZoy1WoxY6SSchcObLxQKbJ9ORi4XNr+IJK5
+3C1Qz/3iv/S3/ktgmqGhQiqybkkHZcbqTXB2wxrx+aaLS7PEfYiuYCrPbX93160k
+MVns8PjvYU8KCNMbL2e+AiKEt1KkKAZIpNQdeeJOEhV9wuLYFosd400aYssuSOVW
+IkJhGI0lT/7FDJaw0LV98DhQtauANPSUQKN5iw6vciwtsaF1kXMfGlMXj58ntiMq
+NizQPR6/Ar1ewLPMh1exDoAfLnCIMk8nbSraW+cebLAZctPugUpfpu3j2LM98aE=
+-----END RSA PRIVATE KEY-----
diff --git a/spec/fixtures/RS256/jwtRS256.key.pub b/spec/fixtures/RS256/jwtRS256.key.pub
new file mode 100644
index 0000000..1a2f63d
--- /dev/null
+++ b/spec/fixtures/RS256/jwtRS256.key.pub
@@ -0,0 +1,14 @@
+-----BEGIN PUBLIC KEY-----
+MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA5hdXV/4YSymY1T9VNvK2
+bWRfulwIty1RnAPNINQmfh3aRRkV+PNrbC2Crji9G0AHmQwgW1bZ3kgkkpIm6RVn
+44fHvBvuXkZ9ABgXw0d2cLIHmwOFxSKmWAm/EW//GszUTLLLsMZUe2udtFJW0jxX
+B2GRY0WVYuo6Oo58RCeP719lw3Ags0YF9/IobxKkGd4BautUPw6ZszAa3o+j0zR7
+4x7ouPxybZAOuPsMxqanyeYJeH4osJjLMYV9qem9uG2sj7GENJ8UszcpmGbqxBhe
+xPEB7mgDeONIF0XJF23zdOf8ANE5mAU2h2v7M6moAfkdUzJ+j48+VT2omHAzAL5y
+Ncmrl2xiWdyoxOw1Y1UmfEmJYV5VgGYyZ12JZRKY+szPT+vR+MDuYxbquF40O7kv
+kFNBfL1yCpzfSQCLnEs4rX8qRzZXciLeyq4Ht5FLuRFgxjA//XI8LAmp0u7gk+Q7
+FUH1UgW3kmJDTG0XaxQxYTBSIO7mcmyjDyBgKVuQmt5E1ycFeteOVdPD/CG/fPYh
+thvc4UytEFwsMdNy3iD6/wuUH68tAKam28UZaOb0qK+00cQQD8fulY9rKtSL10Lv
+JFWUOa/SJyLvk9vUmfvFn182il1nX6GpyxyMmE/FCnH4CT/DjrSZf08mOO8eL5of
+YHMK/oiXr1eODqx+pOwClNsCAwEAAQ==
+-----END PUBLIC KEY-----
diff --git a/spec/helper.rb b/spec/helper.rb
index e3cf480..03d97bc 100644
--- a/spec/helper.rb
+++ b/spec/helper.rb
@@ -1,25 +1,21 @@
-DEBUG = ENV['DEBUG'] == 'true'
+# frozen_string_literal: true
 
-if RUBY_VERSION >= '1.9'
-  require 'simplecov'
-  require 'coveralls'
+DEBUG = ENV['DEBUG'] == 'true'
+RUN_COVERAGE = ENV['CI_CODECOV'] || ENV['CI'].nil?
 
-  SimpleCov.formatters = [SimpleCov::Formatter::HTMLFormatter, Coveralls::SimpleCov::Formatter]
+ruby_version = Gem::Version.new(RUBY_VERSION)
+minimum_version = ->(version) { ruby_version >= Gem::Version.new(version) && RUBY_ENGINE == 'ruby' }
+coverage = minimum_version.call('2.7') && RUN_COVERAGE
+debug = minimum_version.call('2.5') && DEBUG
 
-  SimpleCov.start do
-    add_filter '/spec'
-    minimum_coverage(95)
-  end
-end
-
-if DEBUG && RUBY_VERSION >= '2.6'
-  require 'byebug'
-end
+require 'simplecov' if coverage
+require 'byebug' if debug
 
 require 'oauth2'
 require 'addressable/uri'
 require 'rspec'
 require 'rspec/stubbed_env'
+require 'rspec/pending_for'
 require 'silent_stream'
 
 RSpec.configure do |config|
@@ -30,11 +26,6 @@ end
 
 Faraday.default_adapter = :test
 
-# This is dangerous - HERE BE DRAGONS.
-# It allows us to refer to classes without the namespace, but at what cost?!?
-# TODO: Refactor to use explicit references everywhere
-include OAuth2
-
 RSpec.configure do |conf|
   conf.include SilentStream
 end
diff --git a/spec/oauth2/access_token_spec.rb b/spec/oauth2/access_token_spec.rb
index cb3f45f..a205f44 100644
--- a/spec/oauth2/access_token_spec.rb
+++ b/spec/oauth2/access_token_spec.rb
@@ -1,12 +1,12 @@
-require 'helper'
+# frozen_string_literal: true
 
-describe AccessToken do
+describe OAuth2::AccessToken do
   subject { described_class.new(client, token) }
 
   let(:token) { 'monkey' }
   let(:refresh_body) { MultiJson.encode(:access_token => 'refreshed_foo', :expires_in => 600, :refresh_token => 'refresh_bar') }
   let(:client) do
-    Client.new('abc', 'def', :site => 'https://api.example.com') do |builder|
+    OAuth2::Client.new('abc', 'def', :site => 'https://api.example.com') do |builder|
       builder.request :url_encoded
       builder.adapter :test do |stub|
         VERBS.each do |verb|
@@ -32,7 +32,7 @@ describe AccessToken do
       expect(target.params['foo']).to eq('bar')
     end
 
-    def assert_initialized_token(target) # rubocop:disable Metrics/AbcSize
+    def assert_initialized_token(target)
       expect(target.token).to eq(token)
       expect(target).to be_expires
       expect(target.params.keys).to include('foo')
@@ -52,7 +52,7 @@ describe AccessToken do
       expect(hash).to eq(hash_before)
     end
 
-    it 'initalizes with a form-urlencoded key/value string' do
+    it 'initializes with a form-urlencoded key/value string' do
       kvform = "access_token=#{token}&expires_at=#{Time.now.to_i + 200}&foo=bar"
       target = described_class.from_kvform(client, kvform)
       assert_initialized_token(target)
@@ -72,13 +72,33 @@ describe AccessToken do
       expect(opts).to eq(opts_before)
     end
 
-    it 'initializes with a string expires_at' do
-      future = Time.now.utc + 100_000
-      hash = {:access_token => token, :expires_at => future.iso8601, 'foo' => 'bar'}
-      target = described_class.from_hash(client, hash)
-      assert_initialized_token(target)
-      expect(target.expires_at).to be_a(Integer)
-      expect(target.expires_at).to eql(future.to_i)
+    describe 'expires_at' do
+      let(:expires_at) { 1_361_396_829 }
+      let(:hash) do
+        {
+          :access_token => token,
+          :expires_at => expires_at.to_s,
+          'foo' => 'bar',
+        }
+      end
+
+      it 'initializes with an integer timestamp expires_at' do
+        target = described_class.from_hash(client, hash.merge(:expires_at => expires_at))
+        assert_initialized_token(target)
+        expect(target.expires_at).to eql(expires_at)
+      end
+
+      it 'initializes with a string timestamp expires_at' do
+        target = described_class.from_hash(client, hash)
+        assert_initialized_token(target)
+        expect(target.expires_at).to eql(expires_at)
+      end
+
+      it 'initializes with a string time expires_at' do
+        target = described_class.from_hash(client, hash.merge(:expires_at => Time.at(expires_at).iso8601))
+        assert_initialized_token(target)
+        expect(target.expires_at).to eql(expires_at)
+      end
     end
   end
 
@@ -167,8 +187,8 @@ describe AccessToken do
   describe '#refresh!' do
     let(:access) do
       described_class.new(client, token, :refresh_token => 'abaca',
-                                         :expires_in     => 600,
-                                         :param_name     => 'o_param')
+                                         :expires_in => 600,
+                                         :param_name => 'o_param')
     end
 
     it 'returns a refresh token with appropriate values carried over' do
diff --git a/spec/oauth2/authenticator_spec.rb b/spec/oauth2/authenticator_spec.rb
index e4d6048..4448b98 100644
--- a/spec/oauth2/authenticator_spec.rb
+++ b/spec/oauth2/authenticator_spec.rb
@@ -1,4 +1,4 @@
-require 'helper'
+# frozen_string_literal: true
 
 describe OAuth2::Authenticator do
   subject do
diff --git a/spec/oauth2/client_spec.rb b/spec/oauth2/client_spec.rb
index 62cb4ec..d588426 100644
--- a/spec/oauth2/client_spec.rb
+++ b/spec/oauth2/client_spec.rb
@@ -1,11 +1,11 @@
 # coding: utf-8
+# frozen_string_literal: true
 
-require 'helper'
 require 'nkf'
 
 describe OAuth2::Client do
   subject do
-    described_class.new('abc', 'def', :site => 'https://api.example.com') do |builder|
+    described_class.new('abc', 'def', {:site => 'https://api.example.com'}.merge(options)) do |builder|
       builder.adapter :test do |stub|
         stub.get('/success')             { |env| [200, {'Content-Type' => 'text/awesome'}, 'yay'] }
         stub.get('/reflect')             { |env| [200, {}, env[:body]] }
@@ -13,6 +13,7 @@ describe OAuth2::Client do
         stub.get('/unauthorized')        { |env| [401, {'Content-Type' => 'application/json'}, MultiJson.encode(:error => error_value, :error_description => error_description_value)] }
         stub.get('/conflict')            { |env| [409, {'Content-Type' => 'text/plain'}, 'not authorized'] }
         stub.get('/redirect')            { |env| [302, {'Content-Type' => 'text/plain', 'location' => '/success'}, ''] }
+        stub.get('/redirect_no_loc')     { |_env| [302, {'Content-Type' => 'text/plain'}, ''] }
         stub.post('/redirect')           { |env| [303, {'Content-Type' => 'text/plain', 'location' => '/reflect'}, ''] }
         stub.get('/error')               { |env| [500, {'Content-Type' => 'text/plain'}, 'unknown error'] }
         stub.get('/empty_get')           { |env| [204, {}, nil] }
@@ -24,6 +25,7 @@ describe OAuth2::Client do
 
   let!(:error_value) { 'invalid_token' }
   let!(:error_description_value) { 'bad bad token' }
+  let(:options) { {} }
 
   describe '#initialize' do
     it 'assigns id and secret' do
@@ -44,10 +46,10 @@ describe OAuth2::Client do
     end
 
     it 'is able to pass a block to configure the connection' do
-      connection = double('connection')
       builder = double('builder')
-      allow(connection).to receive(:build).and_yield(builder)
-      allow(Faraday::Connection).to receive(:new).and_return(connection)
+
+      allow(Faraday).to receive(:new).and_yield(builder)
+      allow(builder).to receive(:response)
 
       expect(builder).to receive(:adapter).with(:test)
 
@@ -70,7 +72,7 @@ describe OAuth2::Client do
     it 'allows override of raise_errors option' do
       client = described_class.new('abc', 'def', :site => 'https://api.example.com', :raise_errors => true) do |builder|
         builder.adapter :test do |stub|
-          stub.get('/notfound') { |env| [404, {}, nil] }
+          stub.get('/notfound') { |_env| [404, {}, nil] }
         end
       end
       expect(client.options[:raise_errors]).to be true
@@ -109,6 +111,30 @@ describe OAuth2::Client do
         subject.options[:"#{url_type}_url"] = 'https://api.foo.com/oauth/custom'
         expect(subject.send("#{url_type}_url")).to eq('https://api.foo.com/oauth/custom')
       end
+
+      context 'when a URL with path is used in the site' do
+        let(:options) do
+          {:site => 'https://example.com/blog'}
+        end
+
+        it 'generates an authorization URL relative to the site' do
+          expect(subject.send("#{url_type}_url")).to eq("https://example.com/blog/oauth/#{url_type}")
+        end
+      end
+
+      context 'when a URL with path is used in the site and urls overridden' do
+        let(:options) do
+          {
+            :site => 'https://example.com/blog',
+            :authorize_url => "oauth/#{url_type}/lampoon",
+            :token_url => "oauth/#{url_type}/lampoon",
+          }
+        end
+
+        it 'generates an authorization URL relative to the site' do
+          expect(subject.send("#{url_type}_url")).to eq("https://example.com/blog/oauth/#{url_type}/lampoon")
+        end
+      end
     end
   end
 
@@ -157,6 +183,68 @@ describe OAuth2::Client do
         client.auth_code.get_token('code')
       end
     end
+
+    describe 'custom headers' do
+      context 'string key headers' do
+        it 'adds the custom headers to request' do
+          client = described_class.new('abc', 'def', :site => 'https://api.example.com', :auth_scheme => :request_body) do |builder|
+            builder.adapter :test do |stub|
+              stub.post('/oauth/token') do |env|
+                expect(env.request_headers).to include({'CustomHeader' => 'CustomHeader'})
+                [200, {'Content-Type' => 'application/json'}, '{"access_token":"token"}']
+              end
+            end
+          end
+          header_params = {'headers' => {'CustomHeader' => 'CustomHeader'}}
+          client.auth_code.get_token('code', header_params)
+        end
+      end
+
+      context 'symbol key headers' do
+        it 'adds the custom headers to request' do
+          client = described_class.new('abc', 'def', :site => 'https://api.example.com', :auth_scheme => :request_body) do |builder|
+            builder.adapter :test do |stub|
+              stub.post('/oauth/token') do |env|
+                expect(env.request_headers).to include({'CustomHeader' => 'CustomHeader'})
+                [200, {'Content-Type' => 'application/json'}, '{"access_token":"token"}']
+              end
+            end
+          end
+          header_params = {:headers => {'CustomHeader' => 'CustomHeader'}}
+          client.auth_code.get_token('code', header_params)
+        end
+      end
+
+      context 'string key custom headers with basic auth' do
+        it 'adds the custom headers to request' do
+          client = described_class.new('abc', 'def', :site => 'https://api.example.com') do |builder|
+            builder.adapter :test do |stub|
+              stub.post('/oauth/token') do |env|
+                expect(env.request_headers).to include({'CustomHeader' => 'CustomHeader'})
+                [200, {'Content-Type' => 'application/json'}, '{"access_token":"token"}']
+              end
+            end
+          end
+          header_params = {'headers' => {'CustomHeader' => 'CustomHeader'}}
+          client.auth_code.get_token('code', header_params)
+        end
+      end
+
+      context 'symbol key custom headers with basic auth' do
+        it 'adds the custom headers to request' do
+          client = described_class.new('abc', 'def', :site => 'https://api.example.com') do |builder|
+            builder.adapter :test do |stub|
+              stub.post('/oauth/token') do |env|
+                expect(env.request_headers).to include({'CustomHeader' => 'CustomHeader'})
+                [200, {'Content-Type' => 'application/json'}, '{"access_token":"token"}']
+              end
+            end
+          end
+          header_params = {:headers => {'CustomHeader' => 'CustomHeader'}}
+          client.auth_code.get_token('code', header_params)
+        end
+      end
+    end
   end
 
   describe '#request' do
@@ -212,12 +300,13 @@ describe OAuth2::Client do
       end
     end
 
+    # rubocop:disable Style/RedundantBegin
     it 're-encodes response body in the error message' do
       begin
         subject.request(:get, '/ascii_8bit_encoding')
-      rescue StandardError => ex
-        expect(ex.message.encoding.name).to eq('UTF-8')
-        expect(ex.message).to eq("invalid_request: é\n{\"error\":\"invalid_request\",\"error_description\":\"��\"}")
+      rescue StandardError => e
+        expect(e.message.encoding.name).to eq('UTF-8')
+        expect(e.message).to eq("invalid_request: é\n{\"error\":\"invalid_request\",\"error_description\":\"��\"}")
       end
     end
 
@@ -240,12 +329,14 @@ describe OAuth2::Client do
         expect(e.to_s).to match(/unknown error/)
       end
     end
+    # rubocop:enable Style/RedundantBegin
 
     context 'with ENV' do
       include_context 'with stubbed env'
       before do
         stub_env('OAUTH_DEBUG' => 'true')
       end
+
       it 'outputs to $stdout when OAUTH_DEBUG=true' do
         output = capture(:stdout) do
           subject.request(:get, '/success')
@@ -253,7 +344,7 @@ describe OAuth2::Client do
         logs = [
           '-- request: GET https://api.example.com/success',
           '-- response: Status 200',
-          '-- response: Content-Type: "text/awesome"'
+          '-- response: Content-Type: "text/awesome"',
         ]
         expect(output).to include(*logs)
       end
@@ -286,12 +377,153 @@ describe OAuth2::Client do
       client = stubbed_client(:auth_scheme => :basic_auth) do |stub|
         stub.post('/oauth/token') do |env|
           raise Faraday::Adapter::Test::Stubs::NotFound unless env[:request_headers]['Authorization'] == OAuth2::Authenticator.encode_basic_auth('abc', 'def')
+
           [200, {'Content-Type' => 'application/json'}, MultiJson.encode('access_token' => 'the-token')]
         end
       end
       client.get_token({})
     end
 
+    describe 'extract_access_token option' do
+      let(:client) do
+        client = stubbed_client(:extract_access_token => extract_access_token) do |stub|
+          stub.post('/oauth/token') do
+            [200, {'Content-Type' => 'application/json'}, MultiJson.encode('data' => {'access_token' => 'the-token'})]
+          end
+        end
+      end
+
+      context 'with proc extract_access_token' do
+        let(:extract_access_token) do
+          proc do |client, hash|
+            token = hash['data']['access_token']
+            OAuth2::AccessToken.new(client, token, hash)
+          end
+        end
+
+        it 'returns a configured AccessToken' do
+          token = client.get_token({})
+          expect(token).to be_a OAuth2::AccessToken
+          expect(token.token).to eq('the-token')
+        end
+      end
+
+      context 'with depracted Class.from_hash option' do
+        let(:extract_access_token) do
+          CustomAccessToken = Class.new(OAuth2::AccessToken)
+          CustomAccessToken.define_singleton_method(:from_hash) do |client, hash|
+            token = hash['data']['access_token']
+            OAuth2::AccessToken.new(client, token, hash)
+          end
+          CustomAccessToken
+        end
+
+        it 'returns a configured AccessToken' do
+          token = client.get_token({})
+          expect(token).to be_a OAuth2::AccessToken
+          expect(token.token).to eq('the-token')
+        end
+      end
+    end
+
+    describe ':raise_errors flag' do
+      let(:options) { {} }
+      let(:token_response) { nil }
+      let(:post_args) { [] }
+
+      let(:client) do
+        stubbed_client(options.merge(:raise_errors => raise_errors)) do |stub|
+          stub.post('/oauth/token', *post_args) do
+            # stub 200 response so that we're testing the get_token handling of :raise_errors flag not request
+            [200, {'Content-Type' => 'application/json'}, token_response]
+          end
+        end
+      end
+
+      context 'when set to false' do
+        let(:raise_errors) { false }
+
+        context 'when the request body is nil' do
+          it 'returns a nil :access_token' do
+            expect(client.get_token({})).to eq(nil)
+          end
+        end
+
+        context 'when the request body is missing the access_token' do
+          let(:token_response) { MultiJson.encode('unexpected_access_token' => 'the-token') }
+
+          it 'returns a nil :access_token' do
+            expect(client.get_token({})).to eq(nil)
+          end
+        end
+
+        context 'when the request body has an access token' do
+          let(:token_response) { MultiJson.encode('access_token' => 'the-token') }
+
+          it 'returns the parsed :access_token from body' do
+            token = client.get_token({})
+            expect(token).to be_a OAuth2::AccessToken
+            expect(token.token).to eq('the-token')
+          end
+
+          context 'when :auth_scheme => :request_body' do
+            context 'when arbitrary params are present' do
+              let(:post_args) { ['arbitrary' => 'parameter', 'client_id' => 'abc', 'client_secret' => 'def'] }
+              let(:options) { {:auth_scheme => :request_body} }
+
+              it 'does not affect access token' do
+                token = client.get_token(*post_args)
+                expect(token).to be_a OAuth2::AccessToken
+                expect(token.token).to eq('the-token')
+              end
+            end
+          end
+        end
+
+        context 'when extract_access_token raises an exception' do
+          let(:options) do
+            {
+              :extract_access_token => proc { |client, hash| raise ArgumentError },
+            }
+          end
+
+          it 'returns a nil :access_token' do
+            expect(client.get_token({})).to eq(nil)
+          end
+        end
+      end
+
+      context 'when set to true' do
+        let(:raise_errors) { true }
+
+        context 'when the request body is nil' do
+          it 'raises an error' do
+            expect { client.get_token({}) }.to raise_error OAuth2::Error
+          end
+        end
+
+        context 'when the request body is missing the access_token' do
+          let(:token_response) { MultiJson.encode('unexpected_access_token' => 'the-token') }
+
+          it 'raises an error' do
+            expect { client.get_token({}) }.to raise_error OAuth2::Error
+          end
+        end
+
+        context 'when extract_access_token raises an exception' do
+          let(:options) do
+            {
+              :extract_access_token => proc { |client, hash| raise ArgumentError },
+            }
+          end
+
+          it 'raises an error' do
+            expect { client.get_token({}) }.to raise_error OAuth2::Error
+          end
+        end
+      end
+    end
+
     def stubbed_client(params = {}, &stubs)
       params = {:site => 'https://api.example.com'}.merge(params)
       OAuth2::Client.new('abc', 'def', params) do |builder|
@@ -311,7 +543,7 @@ describe OAuth2::Client do
   context 'with SSL options' do
     subject do
       cli = described_class.new('abc', 'def', :site => 'https://api.example.com', :ssl => {:ca_file => 'foo.pem'})
-      cli.connection.build do |b|
+      cli.connection = Faraday.new(cli.site, cli.options[:connection_opts]) do |b|
         b.adapter :test
       end
       cli
diff --git a/spec/oauth2/mac_token_spec.rb b/spec/oauth2/mac_token_spec.rb
index add5f72..eab2599 100644
--- a/spec/oauth2/mac_token_spec.rb
+++ b/spec/oauth2/mac_token_spec.rb
@@ -1,11 +1,11 @@
-require 'helper'
+# frozen_string_literal: true
 
-describe MACToken do
+describe OAuth2::MACToken do
   subject { described_class.new(client, token, 'abc123') }
 
   let(:token) { 'monkey' }
   let(:client) do
-    Client.new('abc', 'def', :site => 'https://api.example.com') do |builder|
+    OAuth2::Client.new('abc', 'def', :site => 'https://api.example.com') do |builder|
       builder.request :url_encoded
       builder.adapter :test do |stub|
         VERBS.each do |verb|
@@ -26,15 +26,18 @@ describe MACToken do
     end
 
     it 'defaults algorithm to hmac-sha-256' do
+      pending_for(:engine => 'ruby', :versions => '1.9.3', :reason => "Ruby 1.9's OpenSSL uses instance of OpenSSL::Digest")
       expect(subject.algorithm).to be_instance_of(OpenSSL::Digest::SHA256)
     end
 
     it 'handles hmac-sha-256' do
+      pending_for(:engine => 'ruby', :versions => '1.9.3', :reason => "Ruby 1.9's OpenSSL uses instance of OpenSSL::Digest")
       mac = described_class.new(client, token, 'abc123', :algorithm => 'hmac-sha-256')
       expect(mac.algorithm).to be_instance_of(OpenSSL::Digest::SHA256)
     end
 
     it 'handles hmac-sha-1' do
+      pending_for(:engine => 'ruby', :versions => '1.9.3', :reason => "Ruby 1.9's OpenSSL uses instance of OpenSSL::Digest")
       mac = described_class.new(client, token, 'abc123', :algorithm => 'hmac-sha-1')
       expect(mac.algorithm).to be_instance_of(OpenSSL::Digest::SHA1)
     end
@@ -91,7 +94,7 @@ describe MACToken do
     subject { described_class.from_access_token(access_token, 'hello') }
 
     let(:access_token) do
-      AccessToken.new(
+      OAuth2::AccessToken.new(
         client, token,
         :expires_at => 1,
         :expires_in => 1,
diff --git a/spec/oauth2/response_spec.rb b/spec/oauth2/response_spec.rb
index ace9ac5..8e703aa 100644
--- a/spec/oauth2/response_spec.rb
+++ b/spec/oauth2/response_spec.rb
@@ -1,4 +1,4 @@
-require 'helper'
+# frozen_string_literal: true
 
 describe OAuth2::Response do
   describe '#initialize' do
@@ -8,9 +8,9 @@ describe OAuth2::Response do
 
     it 'returns the status, headers and body' do
       response = double('response', :headers => headers,
-                                    :status  => status,
-                                    :body    => body)
-      subject = Response.new(response)
+                                    :status => status,
+                                    :body => body)
+      subject = described_class.new(response)
       expect(subject.headers).to eq(headers)
       expect(subject.status).to eq(status)
       expect(subject.body).to eq(body)
@@ -45,7 +45,7 @@ describe OAuth2::Response do
       headers = {'Content-Type' => 'application/x-www-form-urlencoded'}
       body = 'foo=bar&answer=42'
       response = double('response', :headers => headers, :body => body)
-      subject = Response.new(response)
+      subject = described_class.new(response)
       expect(subject.parsed.keys.size).to eq(2)
       expect(subject.parsed['foo']).to eq('bar')
       expect(subject.parsed['answer']).to eq('42')
@@ -55,7 +55,7 @@ describe OAuth2::Response do
       headers = {'Content-Type' => 'application/json'}
       body = MultiJson.encode(:foo => 'bar', :answer => 42)
       response = double('response', :headers => headers, :body => body)
-      subject = Response.new(response)
+      subject = described_class.new(response)
       expect(subject.parsed.keys.size).to eq(2)
       expect(subject.parsed['foo']).to eq('bar')
       expect(subject.parsed['answer']).to eq(42)
@@ -71,12 +71,16 @@ describe OAuth2::Response do
       expect(MultiJson).not_to receive(:load)
       expect(Rack::Utils).not_to receive(:parse_query)
 
-      subject = Response.new(response)
+      subject = described_class.new(response)
       expect(subject.parsed).to be_nil
     end
   end
 
   context 'with xml parser registration' do
+    before do
+      MultiXml.parser = :rexml
+    end
+
     it 'tries to load multi_xml and use it' do
       expect(described_class.send(:class_variable_get, :@@parsers)[:xml]).not_to be_nil
     end
diff --git a/spec/oauth2/strategy/assertion_spec.rb b/spec/oauth2/strategy/assertion_spec.rb
index 36ea17d..cd54ba0 100644
--- a/spec/oauth2/strategy/assertion_spec.rb
+++ b/spec/oauth2/strategy/assertion_spec.rb
@@ -1,11 +1,14 @@
-require 'helper'
+# frozen_string_literal: true
+
+require 'openssl'
 
 describe OAuth2::Strategy::Assertion do
-  subject { client.assertion }
+  let(:client_assertion) { client.assertion }
 
   let(:client) do
     cli = OAuth2::Client.new('abc', 'def', :site => 'http://api.example.com')
-    cli.connection.build do |b|
+    cli.connection = Faraday.new(cli.site, cli.options[:connection_opts]) do |b|
+      b.request :url_encoded
       b.adapter :test do |stub|
         stub.post('/oauth/token') do |env|
           case @mode
@@ -22,38 +25,88 @@ describe OAuth2::Strategy::Assertion do
 
   let(:params) do
     {
-        :hmac_secret => 'foo',
-        :exp => Time.now.utc.to_i + 3600
+      :hmac_secret => 'foo',
+      :exp => Time.now.utc.to_i + 3600,
     }
   end
 
   describe '#authorize_url' do
     it 'raises NotImplementedError' do
-      expect { subject.authorize_url }.to raise_error(NotImplementedError)
+      expect { client_assertion.authorize_url }.to raise_error(NotImplementedError)
     end
   end
 
   %w[json formencoded].each do |mode|
-    describe "#get_token (#{mode})" do
-      before do
-        @mode = mode
-        @access = subject.get_token(params)
-      end
+    before { @mode = mode }
 
-      it 'returns AccessToken with same Client' do
-        expect(@access.client).to eq(client)
-      end
+    shared_examples_for "get_token #{mode}" do
+      describe "#get_token (#{mode})" do
+        subject(:get_token) { client_assertion.get_token(params) }
 
-      it 'returns AccessToken with #token' do
-        expect(@access.token).to eq('salmon')
+        it 'returns AccessToken with same Client' do
+          expect(get_token.client).to eq(client)
+        end
+
+        it 'returns AccessToken with #token' do
+          expect(get_token.token).to eq('salmon')
+        end
+
+        it 'returns AccessToken with #expires_in' do
+          expect(get_token.expires_in).to eq(600)
+        end
+
+        it 'returns AccessToken with #expires_at' do
+          expect(get_token.expires_at).not_to be_nil
+        end
       end
+    end
 
-      it 'returns AccessToken with #expires_in' do
-        expect(@access.expires_in).to eq(600)
+    it_behaves_like "get_token #{mode}"
+    describe "#build_assertion (#{mode})" do
+      context 'with hmac_secret' do
+        subject(:build_assertion) { client_assertion.build_assertion(params) }
+
+        let(:hmac_secret) { '1883be842495c3b58f68ca71fbf1397fbb9ed2fdf8990f8404a25d0a1b995943' }
+        let(:params) do
+          {
+            :iss => 2345,
+            :aud => 'too',
+            :prn => 'much',
+            :exp => 123_456_789,
+            :hmac_secret => hmac_secret,
+          }
+        end
+        let(:jwt) { 'eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOjIzNDUsImF1ZCI6InRvbyIsInBybiI6Im11Y2giLCJleHAiOjEyMzQ1Njc4OX0.GnZjgcdc5WSWKNW0p9S4GuhpBs3LJCEqjPm6turLG-c' }
+
+        it 'returns JWT' do
+          expect(build_assertion).to eq(jwt)
+        end
+
+        it_behaves_like "get_token #{mode}"
       end
 
-      it 'returns AccessToken with #expires_at' do
-        expect(@access.expires_at).not_to be_nil
+      context 'with private_key' do
+        subject(:build_assertion) { client_assertion.build_assertion(params) }
+
+        let(:private_key_file) { 'spec/fixtures/RS256/jwtRS256.key' }
+        let(:password) { '' }
+        let(:private_key) { OpenSSL::PKey::RSA.new(File.read(private_key_file), password) }
+        let(:params) do
+          {
+            :iss => 2345,
+            :aud => 'too',
+            :prn => 'much',
+            :exp => 123_456_789,
+            :private_key => private_key,
+          }
+        end
+        let(:jwt) { 'eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOjIzNDUsImF1ZCI6InRvbyIsInBybiI6Im11Y2giLCJleHAiOjEyMzQ1Njc4OX0.vJ32OiPVMdJrlNkPw02Y9u6beiFY0Mfndhg_CkEDLtOYn8dscQIEpWoR4GzH8tiQVOQ1fOkqxE95tNIKOTjnIoskmYnfzhzIl9fnfQ_lsEuLC-nq45KhPzSM2wYgF2ZEIjDq51daK70bRPzTBr1Id45cTY-jJSito0lbKXj2nPa_Gs-_vyEU2MSxjiMaIxxccfY4Ow5zN3AUMTKp6LjrpDKFxag3fJ1nrb6iDATa504gyJHVLift3ovhAwYidkA81WnmEtISWBY904CKIcZD9Cx3ifS5bc3JaLAteIBKAAyD8o7D60vOKutsjCMHUCKL357BQ36bW7fmaEtW367Ri-xgOsCY0_HeWp991vrJ-DxhFPeuF-8hn_9KggBzKbA2eKEOOY4iDKSFwjWQUFOcRdvHw9RgbGt0IjY3wdo8CaJVlhynh54YlaLgOFhTBPeMgZdqQUHOztljaK9zubeVkrDGNnGuSuq0KR82KArb1x2z7XyZpxiV5ZatP9SNyhn-YIWk7UeQYXaS0UfsBX7L5T1y_FZj84r7Vl42lj1DfdR5DyGvHfZyHotTnejdIrDuQfDL_bGe24eHsilzuEFaajYmu10hxflZ6Apm-lekRRV47tbxTF1zI5we14XsTeklrTXqgDkSw6gyOoNUJm-cQkJpfdvBgUHYGInC1ttz7NU' }
+
+        it 'returns JWT' do
+          expect(build_assertion).to eq(jwt)
+        end
+
+        it_behaves_like "get_token #{mode}"
       end
     end
   end
diff --git a/spec/oauth2/strategy/auth_code_spec.rb b/spec/oauth2/strategy/auth_code_spec.rb
index bcb1984..465521f 100644
--- a/spec/oauth2/strategy/auth_code_spec.rb
+++ b/spec/oauth2/strategy/auth_code_spec.rb
@@ -1,6 +1,5 @@
 # encoding: utf-8
-
-require 'helper'
+# frozen_string_literal: true
 
 describe OAuth2::Strategy::AuthCode do
   subject { client.auth_code }
diff --git a/spec/oauth2/strategy/base_spec.rb b/spec/oauth2/strategy/base_spec.rb
index fb59e96..87dd326 100644
--- a/spec/oauth2/strategy/base_spec.rb
+++ b/spec/oauth2/strategy/base_spec.rb
@@ -1,4 +1,4 @@
-require 'helper'
+# frozen_string_literal: true
 
 describe OAuth2::Strategy::Base do
   it 'initializes with a Client' do
diff --git a/spec/oauth2/strategy/client_credentials_spec.rb b/spec/oauth2/strategy/client_credentials_spec.rb
index 11d3b52..e5b800e 100644
--- a/spec/oauth2/strategy/client_credentials_spec.rb
+++ b/spec/oauth2/strategy/client_credentials_spec.rb
@@ -1,4 +1,4 @@
-require 'helper'
+# frozen_string_literal: true
 
 describe OAuth2::Strategy::ClientCredentials do
   subject { client.client_credentials }
@@ -11,7 +11,7 @@ describe OAuth2::Strategy::ClientCredentials do
       builder.adapter :test do |stub|
         stub.post('/oauth/token', 'grant_type' => 'client_credentials') do |env|
           client_id, client_secret = Base64.decode64(env[:request_headers]['Authorization'].split(' ', 2)[1]).split(':', 2)
-          client_id == 'abc' && client_secret == 'def' || raise(Faraday::Adapter::Test::Stubs::NotFound)
+          (client_id == 'abc' && client_secret == 'def') || raise(Faraday::Adapter::Test::Stubs::NotFound)
           case @mode
           when 'formencoded'
             [200, {'Content-Type' => 'application/x-www-form-urlencoded'}, kvform_token]
diff --git a/spec/oauth2/strategy/implicit_spec.rb b/spec/oauth2/strategy/implicit_spec.rb
index 7f48ec0..7204212 100644
--- a/spec/oauth2/strategy/implicit_spec.rb
+++ b/spec/oauth2/strategy/implicit_spec.rb
@@ -1,4 +1,4 @@
-require 'helper'
+# frozen_string_literal: true
 
 describe OAuth2::Strategy::Implicit do
   subject { client.implicit }
diff --git a/spec/oauth2/strategy/password_spec.rb b/spec/oauth2/strategy/password_spec.rb
index 0c9a07d..e608474 100644
--- a/spec/oauth2/strategy/password_spec.rb
+++ b/spec/oauth2/strategy/password_spec.rb
@@ -1,11 +1,12 @@
-require 'helper'
+# frozen_string_literal: true
 
 describe OAuth2::Strategy::Password do
   subject { client.password }
 
   let(:client) do
     cli = OAuth2::Client.new('abc', 'def', :site => 'http://api.example.com')
-    cli.connection.build do |b|
+    cli.connection = Faraday.new(cli.site, cli.options[:connection_opts]) do |b|
+      b.request :url_encoded
       b.adapter :test do |stub|
         stub.post('/oauth/token') do |env|
           case @mode
diff --git a/spec/oauth2/version_spec.rb b/spec/oauth2/version_spec.rb
new file mode 100644
index 0000000..854f1bc
--- /dev/null
+++ b/spec/oauth2/version_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+RSpec.describe OAuth2::Version do
+  it 'has a version number' do
+    expect(described_class).not_to be nil
+  end
+
+  it 'can be a string' do
+    expect(described_class.to_s).to be_a(String)
+  end
+
+  it 'allows Constant access' do
+    expect(described_class::VERSION).to be_a(String)
+  end
+
+  it 'is greater than 0.1.0' do
+    expect(Gem::Version.new(described_class) > Gem::Version.new('0.1.0')).to be(true)
+  end
+
+  it 'is not a pre-release' do
+    expect(Gem::Version.new(described_class).prerelease?).to be(false)
+  end
+end