diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml new file mode 100644 index 0000000..f8a8006 --- /dev/null +++ b/.github/workflows/audit.yml @@ -0,0 +1,31 @@ +name: Audit + +# Kept out of ci.yml so a newly published advisory does not block a release that +# is otherwise green. +on: + push: + branches: [master] + pull_request: + branches: [master] + schedule: + # Mondays at 13:00 UTC — catches advisories published against the locked + # oauth2/rack chain without waiting for the next commit. + - cron: "0 13 * * 1" + workflow_dispatch: + +jobs: + audit: + name: Dependency audit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + - name: Check for vulnerable dependencies + run: | + bundle exec bundle-audit update + bundle exec bundle-audit check diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4d053aa --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + branches: [master] + # Lets the release workflow gate publishing on this same matrix. + workflow_call: + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + ruby: ["3.2", "3.3", "3.4"] + + name: Ruby ${{ matrix.ruby }} + steps: + - uses: actions/checkout@v5 + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + + - name: Run the test suite + run: bundle exec rake spec + + - name: Build the gem + run: bundle exec rake build + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..6e32503 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,63 @@ +name: Release + +# Cut a release from the GitHub Releases UI: draft a release, set the tag to `vX.Y.Z` +# (matching lib/omniauth-fdc/version.rb), and publish it. GitHub creates the tag, +# which fires this workflow. +on: + release: + types: [published] + +jobs: + # Never publish a version that has not passed the matrix. + test: + uses: ./.github/workflows/ci.yml + + push: + name: Push gem to RubyGems.org + needs: test + runs-on: ubuntu-latest + + # Publishing uses RubyGems trusted publishing (OIDC), so there is no API key + # secret to rotate and it works with the gemspec's `rubygems_mfa_required`. + # + # One-time setup on rubygems.org: open the gem -> "Trusted publishers" -> + # "Create", with owner `dropstream`, repository `omniauth-fdc`, + # workflow `release.yml`, and environment `release`. + environment: release + + permissions: + id-token: write # mandatory for trusted publishing + # GitHub already created the release and its tag, so nothing here writes to + # the repo: `rake release` sees the tag and skips its own tag-and-push step. + contents: read + + steps: + - uses: actions/checkout@v5 + with: + ref: ${{ github.event.release.tag_name }} + # rubygems/release-gem installs its own short-lived git credentials. + persist-credentials: false + + - uses: ruby/setup-ruby@v1 + with: + ruby-version: .ruby-version + bundler-cache: true + + # `rake release` builds whatever version.rb says, regardless of the tag. If the + # two disagree it would publish one version under another version's release, so + # stop here first. + - name: Verify the release tag matches the gem version + env: + TAG_NAME: ${{ github.event.release.tag_name }} + run: | + gem_version="$(ruby -r./lib/omniauth-fdc/version -e 'print Omniauth::Fdc::VERSION')" + tag_version="${TAG_NAME#v}" + if [ "$gem_version" != "$tag_version" ]; then + echo "::error::Release tag ${TAG_NAME} does not match lib/omniauth-fdc/version.rb (${gem_version})." + exit 1 + fi + echo "Releasing omniauth-fdc ${gem_version}" + + # Builds the gem and runs `gem push` against the gemspec's allowed_push_host. + - name: Push to RubyGems.org + uses: rubygems/release-gem@v1 diff --git a/.gitignore b/.gitignore index 783440f..ce77f97 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ # rspec failure tracking .rspec_status *.gem + +# macOS +.DS_Store diff --git a/.ruby-version b/.ruby-version index 79a6144..7921bd0 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.4.4 +3.4.8 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index c22db40..0000000 --- a/.travis.yml +++ /dev/null @@ -1,7 +0,0 @@ ---- -sudo: false -language: ruby -cache: bundler -rvm: - - 2.4.4 -before_install: gem install bundler -v 1.16.6 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d0f369e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,63 @@ +# Changelog + +All notable changes to this project are documented in this file. + +## [0.2.0] - 2026-08-26 + +### Changed + +- **Breaking:** requires `omniauth ~> 2.0` and `omniauth-oauth2 ~> 1.9` (previously an + unpinned `omniauth-oauth2`, which resolved to `omniauth 1.9` / `oauth2 1.4`). Host + applications must upgrade to OmniAuth 2.x; Rails apps also need + [`omniauth-rails_csrf_protection`](https://github.com/cookpad/omniauth-rails_csrf_protection) + because OmniAuth 2 only accepts `POST` for the request phase. +- Pinned the token endpoint to `auth_scheme: :request_body`. `oauth2 2.0` changed its + default to `:basic_auth`, which would have moved `client_id`/`client_secret` out of + the token request body and changed what Fulfillment.com receives. +- Requires Ruby >= 3.2 (the modern `oauth2` dependency chain requires it). +- Development toolchain: Bundler 4.x, `rake ~> 13.0`, `rspec ~> 3.13`. +- Replaced Travis CI with GitHub Actions, testing Ruby 3.2 through 3.4. +- Releases are now cut from the GitHub Releases UI: publishing a `vX.Y.Z` release reruns + the matrix, checks the tag against `version.rb`, and publishes to RubyGems.org via + trusted publishing (OIDC, no stored API key). + +### Added + +- `LICENSE.txt` (MIT), now declared in the gemspec. +- A default `client_options[:site]` of `https://auth.fulfillment.com`, so the strategy + works without a `setup` lambda. Overriding `client_options[:site]` still wins. +- Integration specs covering the request phase, the token exchange, the `redirect_uri` + override and the auth hash. +- A weekly `bundle-audit` run, so a new advisory against the locked `oauth2`/`rack` + chain surfaces here rather than in a consuming app. + +### Fixed + +- `callback_url` no longer doubles a mount prefix. The override added `script_name` to + `callback_path`, but OmniAuth 2 moved `script_name` into `callback_path` itself, so a + strategy mounted under a prefix (for example Devise's `/users`) would have sent + `redirect_uri=http://host/users/users/auth/fdc/callback`. +- Resolved 39 advisories carried by the old locked dependency tree, including + `omniauth` CVE-2015-9284 and CVE-2020-36599, `oauth2` CVE-2026-54603, `jwt` + CVE-2026-45363, `faraday` CVE-2026-54297, and 30 against `rack 2.2.3`. +- `bin/console` required a non-existent `omniauth/fdc` file. +- Development scripts in `bin/` are no longer packaged as gem executables, and the + committed `.gem` build artifacts are gone from the repo. + +## [0.1.2] - 2020-06-29 + +- `callback_url` is overridden so query params from a configured `redirect_uri` are not + sent to Fulfillment.com (#4). + +## [0.1.1] - 2020-05-20 + +- Raised the `omniauth-oauth2` floor to 1.6 and dropped the upper version constraint. + +## [0.1.0] - 2018-10-15 + +- Initial published strategy. + +[0.2.0]: https://github.com/dropstream/omniauth-fdc/releases/tag/v0.2.0 +[0.1.2]: https://rubygems.org/gems/omniauth-fdc/versions/0.1.2 +[0.1.1]: https://rubygems.org/gems/omniauth-fdc/versions/0.1.1 +[0.1.0]: https://rubygems.org/gems/omniauth-fdc/versions/0.1.0 diff --git a/Gemfile b/Gemfile index 0ca84b9..aee8864 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,14 @@ -source "https://rubygems.org" +# frozen_string_literal: true -git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } +source "https://rubygems.org" # Specify your gem's dependencies in omniauth-fdc.gemspec gemspec + +group :development, :test do + gem "bundler-audit", "~> 0.9" + gem "rack-test", "~> 2.2" + gem "rake", "~> 13.0" + gem "rspec", "~> 3.13" + gem "webmock", "~> 3.25" +end diff --git a/Gemfile.lock b/Gemfile.lock index 88f0089..517e49f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,56 +1,147 @@ PATH remote: . specs: - omniauth-fdc (0.1.2) - omniauth-oauth2 + omniauth-fdc (0.2.0) + omniauth (~> 2.0) + omniauth-oauth2 (~> 1.9) GEM remote: https://rubygems.org/ specs: - diff-lcs (1.3) - faraday (1.0.1) - multipart-post (>= 1.2, < 3) - hashie (4.1.0) - jwt (2.2.1) - multi_json (1.14.1) - multi_xml (0.6.0) - multipart-post (2.1.1) - oauth2 (1.4.4) - faraday (>= 0.8, < 2.0) - jwt (>= 1.0, < 3.0) - multi_json (~> 1.3) + addressable (2.9.0) + public_suffix (>= 2.0.2, < 8.0) + anonymous_loader (0.1.3) + version_gem (~> 1.1, >= 1.1.14) + auth-sanitizer (0.2.3) + version_gem (~> 1.1, >= 1.1.14) + base64 (0.3.0) + bigdecimal (4.1.2) + bundler-audit (0.9.3) + bundler (>= 1.2.0) + thor (~> 1.0) + crack (1.0.1) + bigdecimal + rexml + diff-lcs (1.6.2) + faraday (2.14.3) + faraday-net_http (>= 2.0, < 3.5) + json + logger + faraday-net_http (3.4.4) + net-http (~> 0.5) + hashdiff (1.2.1) + hashie (5.1.0) + logger + json (2.21.2) + jwt (3.2.0) + base64 + logger (1.7.0) + multi_xml (0.9.1) + bigdecimal (>= 3.1, < 5) + net-http (0.9.1) + uri (>= 0.11.1) + oauth2 (2.0.25) + anonymous_loader (~> 0.1, >= 0.1.3) + auth-sanitizer (~> 0.2, >= 0.2.3) + faraday (>= 0.17.3, < 4.0) + jwt (>= 1.0, < 4.0) + logger (~> 1.2) multi_xml (~> 0.5) - rack (>= 1.2, < 3) - omniauth (1.9.1) + rack (>= 1.2, < 4) + snaky_hash (~> 2.0, >= 2.0.7) + version_gem (~> 1.1, >= 1.1.14) + omniauth (2.1.4) hashie (>= 3.4.6) - rack (>= 1.6.2, < 3) - omniauth-oauth2 (1.6.0) - oauth2 (~> 1.1) - omniauth (~> 1.9) - rack (2.2.3) - rake (10.5.0) - rspec (3.8.0) - rspec-core (~> 3.8.0) - rspec-expectations (~> 3.8.0) - rspec-mocks (~> 3.8.0) - rspec-core (3.8.0) - rspec-support (~> 3.8.0) - rspec-expectations (3.8.2) + logger + rack (>= 2.2.3) + rack-protection + omniauth-oauth2 (1.9.0) + oauth2 (>= 2.0.2, < 3) + omniauth (~> 2.0) + public_suffix (7.0.5) + rack (3.2.7) + rack-protection (4.2.1) + base64 (>= 0.1.0) + logger (>= 1.6.0) + rack (>= 3.0.0, < 4) + rack-test (2.2.0) + rack (>= 1.3) + rake (13.4.2) + rexml (3.4.4) + rspec (3.13.2) + rspec-core (~> 3.13.0) + rspec-expectations (~> 3.13.0) + rspec-mocks (~> 3.13.0) + rspec-core (3.13.6) + rspec-support (~> 3.13.0) + rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.8.0) - rspec-mocks (3.8.0) + rspec-support (~> 3.13.0) + rspec-mocks (3.13.8) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.8.0) - rspec-support (3.8.0) + rspec-support (~> 3.13.0) + rspec-support (3.13.7) + snaky_hash (2.0.7) + hashie (>= 0.1.0, < 6) + version_gem (~> 1.1, >= 1.1.14) + thor (1.5.0) + uri (1.1.1) + version_gem (1.1.15) + webmock (3.26.3) + addressable (>= 2.8.0) + crack (>= 0.3.2) + hashdiff (>= 0.4.0, < 2.0.0) PLATFORMS ruby DEPENDENCIES - bundler (~> 1.16) + bundler-audit (~> 0.9) omniauth-fdc! - rake (~> 10.0) - rspec (~> 3.0) + rack-test (~> 2.2) + rake (~> 13.0) + rspec (~> 3.13) + webmock (~> 3.25) + +CHECKSUMS + addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af + anonymous_loader (0.1.3) sha256=084a18e2439144d955447dc11dfc982f41fcd1583ad32d4d55151325dc44cb55 + auth-sanitizer (0.2.3) sha256=db10aac92cfbe4c64ab637eebcbe1d67395d1694798041362173370f59933e3c + base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b + bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd + bundler (4.0.11) sha256=5bcec0fb78302e48d02ee46f10ee6e6942be647ba5b44a6d1ddfda9a240ce785 + bundler-audit (0.9.3) sha256=81c8766c71e47d0d28a0f98c7eed028539f21a6ea3cd8f685eb6f42333c9b4e9 + crack (1.0.1) sha256=ff4a10390cd31d66440b7524eb1841874db86201d5b70032028553130b6d4c7e + diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962 + faraday (2.14.3) sha256=1882247e6766615c8220b4392bf1d27f6ebb63d8e28267587cef1fb0bf37f278 + faraday-net_http (3.4.4) sha256=0e78af151747ed1b00f33e25973b4bc220d7f16c00c39676817c8b12331eb588 + hashdiff (1.2.1) sha256=9c079dbc513dfc8833ab59c0c2d8f230fa28499cc5efb4b8dd276cf931457cd1 + hashie (5.1.0) sha256=c266471896f323c446ea8207f8ffac985d2718df0a0ba98651a3057096ca3870 + json (2.21.2) sha256=1f1d3b7cf2b3ba1a69beca0bb6db13d5438b80bff3cd54cdaaa620b9b07c1c6a + jwt (3.2.0) sha256=5419b1fe37b1da0982bd07051f573a8b8789ab724c2aa7e785e4784a3ed217d7 + logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 + multi_xml (0.9.1) sha256=7ce766b59c17241ed62976caeae1fae9b2431b263398c35396239a68c4a64e57 + net-http (0.9.1) sha256=25ba0b67c63e89df626ed8fac771d0ad24ad151a858af2cc8e6a716ca4336996 + oauth2 (2.0.25) sha256=2f736a2f93c2caa67c1b08dc3c9889bb907d62643f3183fefaa1723cba6a82ac + omniauth (2.1.4) sha256=42a05b0496f0d22e1dd85d42aaf602f064e36bb47a6826a27ab55e5ba608763c + omniauth-fdc (0.2.0) + omniauth-oauth2 (1.9.0) sha256=ed15f6d9d20991807ce114cc5b9c1453bce3645b64e51c68c90cff5ff153fee8 + public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623 + rack (3.2.7) sha256=93e13e1c24f93556671d85d2d79fa228c3485815c50d7e2f265b5330c6528fb7 + rack-protection (4.2.1) sha256=cf6e2842df8c55f5e4d1a4be015e603e19e9bc3a7178bae58949ccbb58558bac + rack-test (2.2.0) sha256=005a36692c306ac0b4a9350355ee080fd09ddef1148a5f8b2ac636c720f5c463 + rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701 + rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142 + rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587 + rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d + rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836 + rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47 + rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c + snaky_hash (2.0.7) sha256=7d02c70012a3f932e48860cd024577908300c9aa615e0cb9b450aaa749cbcb4d + thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73 + uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6 + version_gem (1.1.15) sha256=a73241587b29252e3567e0c818ded80730eb754d43b508f6ce4db22ca9ba27d0 + webmock (3.26.3) sha256=77c612816bdcb2fc43db0e42fd98b19be7d4770fd708816fdde65a006664ddec BUNDLED WITH - 1.17.3 + 4.0.11 diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..1c0e47b --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018-2026 Dropstream + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md index c275528..294e110 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Omniauth::Fdc -Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/omniauth/fdc`. To experiment with that code, run `bin/console` for an interactive prompt. +This is an OmniAuth strategy for authenticating to [Fulfillment.com](https://www.fulfillment.com). -TODO: Delete this and the text above, and describe your gem +Requires Ruby >= 3.2, OmniAuth 2.x and `omniauth-oauth2` 1.9.x. ## Installation @@ -22,14 +22,114 @@ Or install it yourself as: ## Usage -TODO: Write usage instructions here +OmniAuth 2 only accepts `POST` for the request phase. In a Rails app, add +[`omniauth-rails_csrf_protection`](https://github.com/cookpad/omniauth-rails_csrf_protection) +to your Gemfile and link to the provider with `button_to` or `link_to ..., method: :post`: + +```ruby +gem 'omniauth-rails_csrf_protection' +``` + +```ruby +Rails.application.config.middleware.use OmniAuth::Builder do + provider :fdc, + 'client_id', 'client_secret', + :scope => 'oms' +end +``` + +The auth hash carries the token under `credentials`: + +```ruby +auth = request.env['omniauth.auth'] +auth['credentials']['token'] +``` + +## Configuring + +### Configure the auth host + +`client_options[:site]` defaults to `https://auth.fulfillment.com`. Point it elsewhere to +authenticate against a different Fulfillment.com environment: + +```ruby +Rails.application.config.middleware.use OmniAuth::Builder do + provider :fdc, + 'client_id', 'client_secret', + :scope => 'oms', + :client_options => { :site => 'https://auth.staging.fulfillment.com' } +end +``` + +Setting it from a `setup` lambda also still works, which is how apps configured the host +before the default existed: + +```ruby + :setup => ->(env) { env['omniauth.strategy'].options[:client_options][:site] = 'https://auth.fulfillment.com' } +``` + +### Configure the callback URL + +The strategy overrides `callback_url` to drop the query string that OmniAuth would +otherwise append, because Fulfillment.com rejects a `redirect_uri` that does not match +the registered one exactly. Pass `callback_url` to override it outright: + +```ruby + :callback_url => 'https://app.example.com/users/auth/fdc/callback' +``` + +Otherwise it is derived from the request, including the mount prefix — a strategy mounted +under Devise's `/users` produces `https://app.example.com/users/auth/fdc/callback`. + +### Client credentials in the token request + +`client_options[:auth_scheme]` is pinned to `:request_body`, because Fulfillment.com +expects `client_id` and `client_secret` in the token request body. This was `oauth2` 1.x's +default; `oauth2` 2.x defaults to `:basic_auth`. Do not override it unless +Fulfillment.com starts accepting HTTP Basic auth on the token endpoint. ## 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 `bin/setup` to install dependencies. Then, run `bundle exec rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. -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). +To install this gem onto your local machine, run `bundle exec rake install`. + +Run `bundle exec bundle-audit check --update` to check the locked dependency tree against +the advisory database; CI runs the same check weekly. + +## Releasing + +Releases are cut from the [GitHub Releases UI](https://github.com/dropstream/omniauth-fdc/releases/new); +GitHub Actions builds and publishes the gem to [rubygems.org](https://rubygems.org) via +[RubyGems trusted publishing](https://guides.rubygems.org/trusted-publishing/), so no API +key is stored in the repo and the gemspec can keep `rubygems_mfa_required`. + +1. Bump `VERSION` in `lib/omniauth-fdc/version.rb` and add a `CHANGELOG.md` section + for it, along with a link definition for the new heading at the bottom of that file. + Commit both on `master`. +2. Draft a new release with the target set to `master` and the tag set to the version + prefixed with `v` — `v0.2.0`, not `0.2.0`. The prefix is stripped before comparing + against `version.rb`; the gem version itself carries no `v`. +3. Click **Generate release notes** — the preferred method — then publish. The generated + notes list the merged pull requests; `CHANGELOG.md` carries the curated prose and + links back to each release. + +Publishing creates the tag, which fires the `Release` workflow: it reruns the full test +matrix, refuses to continue if the tag and `version.rb` disagree, and pushes the gem. +Note that RubyGems versions are immutable — a bad release can be yanked, never replaced, +so the version bump has to be committed before the release is published. + +### One-time RubyGems setup + +Needed before the first release from CI, and again only if the gem is renamed or moved: +on rubygems.org open the gem -> **Trusted publishers** -> **Create**, with owner +`dropstream`, repository `omniauth-fdc`, workflow `release.yml`, and environment +`release`. The repo also needs an environment named `release`. ## Contributing -Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/omniauth-fdc. +Bug reports and pull requests are welcome on GitHub at https://github.com/dropstream/omniauth-fdc. + +## License + +Available as open source under the terms of the [MIT License](LICENSE.txt). diff --git a/bin/console b/bin/console index e543eb7..e276177 100755 --- a/bin/console +++ b/bin/console @@ -1,14 +1,11 @@ #!/usr/bin/env ruby +# frozen_string_literal: true require "bundler/setup" -require "omniauth/fdc" +require "omniauth-fdc" # You can add fixtures and/or initialization code here to make experimenting # with your gem easier. You can also use a different console, if you like. -# (If you use this, don't forget to add pry to your Gemfile!) -# require "pry" -# Pry.start - require "irb" IRB.start(__FILE__) diff --git a/lib/omniauth-fdc.rb b/lib/omniauth-fdc.rb index 9a96005..b0f92fc 100644 --- a/lib/omniauth-fdc.rb +++ b/lib/omniauth-fdc.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + require "omniauth-fdc/version" require 'omniauth/strategies/fdc' diff --git a/lib/omniauth-fdc/version.rb b/lib/omniauth-fdc/version.rb index b8fc680..84db485 100644 --- a/lib/omniauth-fdc/version.rb +++ b/lib/omniauth-fdc/version.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + module Omniauth module Fdc - VERSION = "0.1.2" + VERSION = "0.2.0" end end diff --git a/lib/omniauth/strategies/fdc.rb b/lib/omniauth/strategies/fdc.rb index 02c22a7..f10c7c0 100644 --- a/lib/omniauth/strategies/fdc.rb +++ b/lib/omniauth/strategies/fdc.rb @@ -1,18 +1,28 @@ +# frozen_string_literal: true + require 'omniauth-oauth2' module OmniAuth module Strategies class Fdc < OmniAuth::Strategies::OAuth2 + DEFAULT_SITE = 'https://auth.fulfillment.com' option :client_options, { + :site => DEFAULT_SITE, :authorize_url => '/oauth/authorize', :token_url => '/oauth/access_token', - } - + # oauth2 2.x defaults to :basic_auth; Fulfillment.com expects the client + # credentials in the token request body, which was the oauth2 1.x default. + :auth_scheme => :request_body + } + + # OmniAuth::Strategy#callback_url appends the callback request's own query + # string, which would send those params to Fulfillment.com as part of + # redirect_uri and break the exchange. Note that omniauth 2's `callback_path` + # already carries SCRIPT_NAME, so mount prefixes must not be added again. def callback_url - options[:callback_url] || (full_host + script_name + callback_path) + options[:callback_url] || (full_host + callback_path) end - end end end diff --git a/omniauth-fdc.gemspec b/omniauth-fdc.gemspec index 7011f82..e168337 100644 --- a/omniauth-fdc.gemspec +++ b/omniauth-fdc.gemspec @@ -1,5 +1,6 @@ +# frozen_string_literal: true -lib = File.expand_path("../lib", __FILE__) +lib = File.expand_path("lib", __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require "omniauth-fdc/version" @@ -9,31 +10,27 @@ Gem::Specification.new do |spec| spec.authors = ["Sreejith Pillai"] spec.email = ["sreejith.gp@icloud.com"] - spec.summary = %q{OmniAuth strategy for Fulfillment.com} - spec.description = %q{In this gem you will find an OmniAuth Fdc strategy} + spec.summary = "OmniAuth strategy for Fulfillment.com" + spec.description = "In this gem you will find an OmniAuth Fdc strategy" spec.homepage = "https://github.com/dropstream/omniauth-fdc" + spec.license = "MIT" - # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host' - # to allow pushing to a single host or delete this section to allow pushing to any host. - if spec.respond_to?(:metadata) - spec.metadata["allowed_push_host"] = "https://rubygems.org" - else - raise "RubyGems 2.0 or newer is required to protect against " \ - "public gem pushes." - end + spec.required_ruby_version = ">= 3.2" + + spec.metadata["allowed_push_host"] = "https://rubygems.org" + spec.metadata["homepage_uri"] = spec.homepage + spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md" + spec.metadata["rubygems_mfa_required"] = "true" - # Specify which files should be added to the gem when it is released. - # The `git ls-files -z` loads the files in the RubyGem that have been added into git. - spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do - `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } + # Ship only the library itself; development scaffolding stays out of the gem. + spec.files = Dir.chdir(__dir__) do + `git ls-files -z`.split("\x0").reject do |f| + f.match(%r{\A(?:spec|bin|pkg|\.github)/}) || + f.match(%r{\A(?:\.gitignore|\.rspec|\.ruby-version|\.ruby-gemset|Gemfile|Gemfile\.lock|Rakefile)\z}) + end end - spec.bindir = "exe" - spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] - spec.add_runtime_dependency 'omniauth-oauth2' - - spec.add_development_dependency "bundler", "~> 1.16" - spec.add_development_dependency "rake", "~> 10.0" - spec.add_development_dependency "rspec", "~> 3.0" + spec.add_dependency "omniauth", "~> 2.0" + spec.add_dependency "omniauth-oauth2", "~> 1.9" end diff --git a/spec/omniauth/fdc_spec.rb b/spec/omniauth/fdc_spec.rb index bce95db..cfaa26b 100644 --- a/spec/omniauth/fdc_spec.rb +++ b/spec/omniauth/fdc_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + RSpec.describe Omniauth::Fdc do it "has a version number" do expect(Omniauth::Fdc::VERSION).not_to be nil diff --git a/spec/omniauth/strategies/fdc_spec.rb b/spec/omniauth/strategies/fdc_spec.rb index c3f7458..87f8a5b 100644 --- a/spec/omniauth/strategies/fdc_spec.rb +++ b/spec/omniauth/strategies/fdc_spec.rb @@ -1,20 +1,171 @@ +# frozen_string_literal: true + RSpec.describe OmniAuth::Strategies::Fdc do + let(:inner_app) { ->(_env) { [200, {}, ["Hello."]] } } + let(:strategy_options) { {} } + let(:strategy) { described_class.new(inner_app, "client_id", "client_secret", **strategy_options) } + def app - lambda do |_env| - [200, {}, ["Hello."]] - end + described_class.new(inner_app, "client_id", "client_secret", **strategy_options) end - let(:strategy) { Class.new(OmniAuth::Strategies::Fdc) } describe "#client_options" do - subject { strategy.new(app).options.client_options } + subject(:client_options) { strategy.options.client_options } + + it "points at the Fulfillment.com auth site" do + expect(client_options.site).to eq("https://auth.fulfillment.com") + end + + it "has the correct authorize url" do + expect(client_options.authorize_url).to eq("/oauth/authorize") + end - it 'should have correct authorize url' do - expect(subject.authorize_url).to eq('/oauth/authorize') + it "has the correct token url" do + expect(client_options.token_url).to eq("/oauth/access_token") end - it 'should have correct token url' do - expect(subject.token_url).to eq('/oauth/access_token') + # oauth2 >= 2.0 defaults to :basic_auth, which would move the client + # credentials out of the token request body. + it "sends client credentials in the request body" do + expect(client_options.auth_scheme).to eq(:request_body) end + + context "when the site is overridden" do + let(:strategy_options) { {client_options: {site: "https://auth.staging.fulfillment.com"}} } + + it "uses the configured site" do + expect(client_options.site).to eq("https://auth.staging.fulfillment.com") + end + end + end + + describe "the request phase" do + around do |example| + previous = OmniAuth.config.request_validation_phase + OmniAuth.config.request_validation_phase = proc {} + example.run + OmniAuth.config.request_validation_phase = previous + end + + it "redirects to Fulfillment.com's authorize endpoint" do + post "/auth/fdc", {}, "rack.session" => {} + + expect(last_response.status).to eq(302) + location = URI.parse(last_response.headers["Location"]) + expect("#{location.scheme}://#{location.host}#{location.path}") + .to eq("https://auth.fulfillment.com/oauth/authorize") + expect(URI.decode_www_form(location.query).to_h) + .to include("client_id" => "client_id", "response_type" => "code") + end + + context "when a scope is configured" do + let(:strategy_options) { {scope: "oms"} } + + it "passes the scope through to the authorize url" do + post "/auth/fdc", {}, "rack.session" => {} + + query = URI.decode_www_form(URI.parse(last_response.headers["Location"]).query).to_h + expect(query["scope"]).to eq("oms") + end + end + + context "when configured for another site" do + let(:strategy_options) { {client_options: {site: "https://auth.staging.fulfillment.com"}} } + + it "redirects to the configured auth site" do + post "/auth/fdc", {}, "rack.session" => {} + + expect(last_response.headers["Location"]) + .to start_with("https://auth.staging.fulfillment.com/oauth/authorize") + end + end + end + + describe "the callback phase" do + let(:session) { {"omniauth.state" => "abc123"} } + let(:token_url) { "#{strategy.options.client_options[:site]}/oauth/access_token" } + + before do + stub_request(:post, token_url).to_return( + status: 200, + body: {access_token: "token123", token_type: "Bearer"}.to_json, + headers: {"Content-Type" => "application/json"} + ) + end + + def complete_callback(query = "") + post "/auth/fdc/callback#{query}", + {"code" => "auth_code", "state" => "abc123"}, + "rack.session" => session + end + + def token_request_body + body = nil + expect(WebMock).to have_requested(:post, token_url).with { |req| body = req.body; true } + URI.decode_www_form(body).to_h + end + + it "exchanges the code for a token with credentials in the body" do + complete_callback + + expect(token_request_body.values_at("client_id", "client_secret")) + .to eq(%w[client_id client_secret]) + end + + it "does not fall back to HTTP basic auth for the client credentials" do + complete_callback + + expect(WebMock).to have_requested(:post, token_url) + .with { |req| !req.headers.key?("Authorization") } + end + + # PR #4: the redirect_uri sent to Fulfillment.com must not carry the callback's query params. + it "sends a redirect_uri without query params" do + complete_callback("?utm_source=fdc") + + expect(token_request_body["redirect_uri"]).to eq("http://example.org/auth/fdc/callback") + end + + it "builds an auth hash carrying the token" do + complete_callback + + credentials = last_request.env["omniauth.auth"]["credentials"] + expect(credentials["token"]).to eq("token123") + expect(credentials["expires"]).to be(false) + end + + # OmniAuth's own callback_url drops SCRIPT_NAME, which would send the wrong + # redirect_uri when the strategy is mounted under a prefix (e.g. Devise's /users). + it "keeps the mount point in the redirect_uri" do + post "/auth/fdc/callback", + {"code" => "auth_code", "state" => "abc123"}, + "rack.session" => session, "SCRIPT_NAME" => "/users" + + expect(token_request_body["redirect_uri"]).to eq("http://example.org/users/auth/fdc/callback") + end + + context "when configured for another site" do + let(:strategy_options) { {client_options: {site: "https://auth.staging.fulfillment.com"}} } + + it "exchanges the token against the configured host" do + complete_callback + + expect(WebMock).to have_requested(:post, "https://auth.staging.fulfillment.com/oauth/access_token") + expect(last_request.env["omniauth.auth"]["credentials"]["token"]).to eq("token123") + end + end + end + + describe "#callback_url" do + subject(:callback_url) { strategy.callback_url } + + context "when callback_url is configured" do + let(:strategy_options) { {callback_url: "https://app.getdropstream.com/users/auth/fdc/callback"} } + + it "uses the configured value verbatim" do + expect(callback_url).to eq("https://app.getdropstream.com/users/auth/fdc/callback") + end + end + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 8d88324..4b07fd4 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,7 +1,15 @@ +# frozen_string_literal: true + require "bundler/setup" +require "rack/test" +require "webmock/rspec" require "omniauth-fdc" +OmniAuth.config.logger = Logger.new(File::NULL) + RSpec.configure do |config| + config.include Rack::Test::Methods + # Enable flags like --only-failures and --next-failure config.example_status_persistence_file_path = ".rspec_status"