Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .github/workflows/audit.yml
Original file line number Diff line number Diff line change
@@ -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
56 changes: 56 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
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", "4.0"]

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

# The gemspec deliberately allows a wide dependency range, so the floor gets the
# same suite rather than being taken on trust.
floor:
name: Oldest supported dependencies
runs-on: ubuntu-latest
env:
BUNDLE_GEMFILE: gemfiles/floor.gemfile

steps:
- uses: actions/checkout@v5

- uses: ruby/setup-ruby@v1
with:
ruby-version: .ruby-version

- name: Install the oldest allowed dependencies
run: bundle install --jobs 4

- name: Show what resolved
run: bundle list

- name: Run the test suite
run: bundle exec rake spec
68 changes: 68 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
name: Release

# Cut a release from the GitHub Releases UI: draft a release, set the tag to `vX.Y.Z`
# (matching lib/omniauth-shopline/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`.
#
# omniauth-shopline has never been pushed, so the first release needs a *pending*
# trusted publisher instead of a per-gem one — it both claims the name and makes
# the pusher an owner:
# https://rubygems.org/profile/oidc/pending_trusted_publishers -> "Create", with
# gem name `omniauth-shopline`, owner `dropstream`, repository `omniauth-shopline`,
# workflow `release.yml`, and environment `release`.
# After the first successful push it becomes an ordinary trusted publisher, managed
# from the gem's own "Trusted publishers" page.
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-shopline/version -e 'print Omniauth::Shopline::VERSION')"
tag_version="${TAG_NAME#v}"
if [ "$gem_version" != "$tag_version" ]; then
echo "::error::Release tag ${TAG_NAME} does not match lib/omniauth-shopline/version.rb (${gem_version})."
exit 1
fi
echo "Releasing omniauth-shopline ${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
17 changes: 17 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/.bundle/
/.yardoc
/_yardoc/
/coverage/
/doc/
/pkg/
/spec/reports/
/tmp/

# rspec failure tracking
.rspec_status

*.gem
gemfiles/*.lock

# macOS
.DS_Store
3 changes: 3 additions & 0 deletions .rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
--format documentation
--color
--require spec_helper
1 change: 1 addition & 0 deletions .ruby-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.4.8
78 changes: 78 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Changelog

All notable changes to this project are documented in this file. Each version heading
links to its [GitHub release](https://github.com/dropstream/omniauth-shopline/releases),
where the notes are generated from the merged pull requests.

## [0.1.0] - 2026-08-27

First release published to RubyGems.org. Everything below is relative to the state of
`master` before it, which is how the gem was installed until now (`git:` in a Gemfile).

### Changed

- Runtime dependencies are declared as `omniauth >= 2.0, < 4` and
`omniauth-oauth2 >= 1.8, < 3`. A plugin's ceiling becomes a ceiling in every
application that installs it, so this allows the next major of both gems rather than
blocking consumers on the day one ships. The floors are where OmniAuth 2 support
actually begins — `omniauth-oauth2` 1.8.0 is its first release to require
`omniauth ~> 2.0` — and CI runs the suite against those floors as well as against the
newest resolvable versions.
- Requires Ruby >= 3.2, tested on 3.2 through 4.0.
- The app key and secret are read from the `client_id`/`client_secret` positional
arguments, with `app_key`/`app_secret` accepted as named options. Previously the
strategy read `options.app_key`, which the positional arguments never set, so every
request went out with an empty `appKey`.
- `client_options[:site]` is only derived from `handle` when it still holds the
`{handle}` placeholder, so an explicitly configured site now survives.
- Removed the unused `token_params` default (`grant_type: authorization_code`). The
token endpoint takes only `{"code": ...}`, so the option never reached SHOPLINE.
- Development toolchain: Bundler 4.x, `rake ~> 13.0`, `rspec ~> 3.13`, `webmock ~> 3.25`.
- Added GitHub Actions for CI, a weekly `bundle-audit` run, and releases published to
RubyGems.org from the GitHub Releases UI via trusted publishing (OIDC, no stored API
key).

### Added

- `LICENSE.txt` (MIT) is declared in the gemspec, along with `required_ruby_version` and
the usual `homepage`/`changelog`/`rubygems_mfa_required` metadata.
- CSRF protection for the callback. SHOPLINE has no `state` parameter, so the nonce
travels in `customField` — the documented pass-through — and is verified against the
session. Disable with `verify_state: false`.
- Verification of the `sign` SHOPLINE appends to the callback query, computed the way
[SHOPLINE's own SDK](https://github.com/shoplineos/shopline-sdk-go/blob/main/client/sign.go)
computes it. Disable with `verify_signature: false`.
- `scope` in the credentials hash, which the README documented but the code did not
provide.
- `handle` is exposed as a reader on the strategy.
- Integration specs covering the request phase, the token exchange and its signature,
state and signature verification, the auth hash, and the `callback_url` overrides.

### Fixed

- The authorize URL put its query string in front of the `#` fragment
(`/admin/oauth-web/?appKey=...#/oauth/authorize`), because `OAuth2::Client#authorize_url`
cannot build a URL for a hash-routed page. SHOPLINE's admin page never read those
params. It is now built by hand as
`/admin/oauth-web/#/oauth/authorize?appKey=...`.
- The token request was signed with the sorted-parameter scheme SHOPLINE uses for GETs
(`appkey=..&timestamp=..`). A POST is signed over the request body concatenated with
the millisecond timestamp, and the signed string is now the exact body that is sent.
- Every callback failed with `csrf_detected`: the request phase never wrote
`omniauth.state`, and SHOPLINE does not echo a `state` parameter, so
`OmniAuth::Strategies::OAuth2#callback_phase` rejected the callback before the token
exchange.
- `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
`redirectUri=http://host/users/users/auth/shopline/callback`.
- A token response without an access token used to `fail!` from inside
`build_access_token` and hand the resulting Rack triple back as the access token. It
now raises `OAuth2::Error`, which OmniAuth turns into an `invalid_credentials` failure.
- The callback nonce and signature are compared in constant time.
- The token's `scope` reaches `OAuth2::AccessToken` under a string key. It was passed
with a symbol key, which `access_token.params['scope']` never found, so
`extra['scope']` was always `nil`.
- Development scripts in `bin/` and the spec suite are no longer packaged into the gem.

[0.1.0]: https://github.com/dropstream/omniauth-shopline/releases/tag/v0.1.0
14 changes: 12 additions & 2 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
source 'https://rubygems.org'
# frozen_string_literal: true

source "https://rubygems.org"

# Specify your gem's dependencies in omniauth-shopline.gemspec
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
Loading