Skip to content

Commit

Permalink
Merge branch 'main' into liz/route-cli-webhooks
Browse files Browse the repository at this point in the history
  • Loading branch information
lizkenyon committed Feb 5, 2024
2 parents 70b3390 + 6e2da43 commit 715ee97
Show file tree
Hide file tree
Showing 26 changed files with 255 additions and 41 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
steps:
- name: Extract tag name
id: tag
run: echo "::set-output name=value::${GITHUB_REF##*/}"
run: echo "value=${GITHUB_REF##*/}" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@v3

- name: Create Release
Expand Down
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ Unreleased
----------
* Make type param for webhooks route optional. This will fix a bug with CLI initiated webhooks.

21.9.0 (January 16, 2023)
21.10.0 (January 24, 2024)
----------
* Fix session deletion for users with customized session storage[#1773](https://github.com/Shopify/shopify_app/pull/1773)
* Add configuration flag `check_session_expiry_date` to trigger a re-auth when the (user) session is expired. The session expiry date must be stored and retrieved for this flag to be effective. When the `UserSessionStorageWithScopes` concern is used, a DB migration can be generated with `rails generate shopify_app:user_model --skip` and should be applied before enabling that flag[#1757](https://github.com/Shopify/shopify_app/pull/1757)

21.9.0 (January 16, 2024)
----------
* Fix `add_webhook` generator to create the webhook jobs under the correct directory[#1748](https://github.com/Shopify/shopify_app/pull/1748)
* Add support for metafield_namespaces in webhook registration [#1745](https://github.com/Shopify/shopify_app/pull/1745)
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
shopify_app (21.9.0)
shopify_app (21.10.0)
activeresource
addressable (~> 2.7)
browser_sniffer (~> 2.0)
Expand Down
4 changes: 2 additions & 2 deletions docs/shopify_app/generators.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,13 @@ Specify whether the app is an embedded app. Apps are embedded by default.

#### `$ rails generate shopify_app:shop_model`

This generator creates a `Shop` model and a migration to store shop installation records. See [*Shop-based token strategy*](/docs/shopify_app/session-repository.md#shop-based-token-storage) to learn more.
This generator creates a `Shop` model and a migration to store shop installation records. See [*Shop-based token strategy*](/docs/shopify_app/sessions.md#shop-offline-token-storage) to learn more.

---

#### `$ rails generate shopify_app:user_model`

This generator creates a `User` model and a migration to store user records. See [*User-based token strategy*](/docs/shopify_app/session-repository.md#user-based-token-storage) to learn more.
This generator creates a `User` model and a migration to store user records. See [*User-based token strategy*](/docs/shopify_app/sessions.md#user-online-token-storage) to learn more.

---

Expand Down
43 changes: 26 additions & 17 deletions docs/shopify_app/sessions.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,27 @@ Sessions are used to make contextual API calls for either a shop (offline sessio

#### Table of contents

- [Sessions](#sessions-1)
- [Types of session tokens](#types-of-session-tokens) - Shop (offline) v.s. User (online)
- [Session token storage](#session-token-storage)
- [Shop (offline) token storage](#shop-offline-token-storage)
- [User (online) token storage](#user-online-token-storage)
- [In-Memory Session Storage for Testing](#in-memory-session-storage-for-testing)
- [Customizing Session Storage with `ShopifyApp::SessionRepository`](#customizing-session-storage-with-shopifyappsessionrepository)
- [Loading Sessions](#loading-sessions)
- [Sessions](#sessions)
- [Table of contents](#table-of-contents)
- [Sessions](#sessions-1)
- [Types of session tokens](#types-of-session-tokens)
- [Session token storage](#session-token-storage)
- [Shop (offline) token storage](#shop-offline-token-storage)
- [User (online) token storage](#user-online-token-storage)
- [In-memory Session Storage for testing](#in-memory-session-storage-for-testing)
- [Customizing Session Storage with `ShopifyApp::SessionRepository`](#customizing-session-storage-with-shopifyappsessionrepository)
- [⚠️ Custom Session Storage Requirements](#️--custom-session-storage-requirements)
- [Available `ActiveSupport::Concerns` that contains implementation of the above methods](#available-activesupportconcerns-that-contains-implementation-of-the-above-methods)
- [Loading Sessions](#loading-sessions)
- [Getting Sessions with Controller Concerns](#getting-sessions-with-controller-concerns)
- [Shop session - "EnsureInstalled" ](#shop-sessions---ensureinstalled)
- [User session - "EnsureHasSession" ](#user-sessions---ensurehassession)
- [Getting Sessions from a Shop or User model record - "with_shopify_session"](#getting-sessions-from-a-shop-or-user-model-record---with_shopify_session)
- [Access scopes](#access-scopes)
- [`ShopifyApp::ShopSessionStorageWithScopes`](#shopifyappshopsessionstoragewithscopes)
- [``ShopifyApp::UserSessionStorageWithScopes``](#shopifyappusersessionstoragewithscopes)
- [Migrating from shop-based to user-based token strategy](#migrating-from-shop-based-to-user-based-token-strategy)
- [Migrating from ShopifyApi::Auth::SessionStorage to ShopifyApp::SessionStorage](#migrating-from-shopifyapiauthsessionstorage-to-shopifyappsessionstorage)
- [**Shop Sessions - `EnsureInstalled`**](#shop-sessions---ensureinstalled)
- [User Sessions - `EnsureHasSession`](#user-sessions---ensurehassession)
- [Getting sessions from a Shop or User model record - 'with\_shopify\_session'](#getting-sessions-from-a-shop-or-user-model-record---with_shopify_session)
- [Access scopes](#access-scopes)
- [`ShopifyApp::ShopSessionStorageWithScopes`](#shopifyappshopsessionstoragewithscopes)
- [`ShopifyApp::UserSessionStorageWithScopes`](#shopifyappusersessionstoragewithscopes)
- [Migrating from shop-based to user-based token strategy](#migrating-from-shop-based-to-user-based-token-strategy)
- [Migrating from `ShopifyApi::Auth::SessionStorage` to `ShopifyApp::SessionStorage`](#migrating-from-shopifyapiauthsessionstorage-to-shopifyappsessionstorage)

## Sessions
#### Types of session tokens
Expand Down Expand Up @@ -103,13 +107,15 @@ The custom **Shop** repository must implement the following methods:
| `self.store(auth_session)` | `auth_session` (ShopifyAPI::Auth::Session) | - |
| `self.retrieve(id)` | `id` (String) | ShopifyAPI::Auth::Session |
| `self.retrieve_by_shopify_domain(shopify_domain)` | `shopify_domain` (String) | ShopifyAPI::Auth::Session |
| `self.destroy_by_shopify_domain(shopify_domain)` | `shopify_domain` (String) | - |

The custom **User** repository must implement the following methods:
| Method | Parameters | Return Type |
|---------------------------------------------|-------------------------------------|------------------------------|
| `self.store(auth_session, user)` | <li>`auth_session` (ShopifyAPI::Auth::Session)<br><li>`user` (ShopifyAPI::Auth::AssociatedUser) | - |
| `self.retrieve(id)` | `id` (String) | `ShopifyAPI::Auth::Session` |
| `self.retrieve_by_shopify_user_id(user_id)` | `user_id` (String) | `ShopifyAPI::Auth::Session` |
| `self.destroy_by_shopify_user_id(user_id)` | `user_id` (String) | - |


These methods are already implemented as a part of the `User` and `Shop` models generated from this gem's generator.
Expand Down Expand Up @@ -153,7 +159,7 @@ end
```

##### User Sessions - `EnsureHasSession`
- [EnsureHasSession](https://github.com/Shopify/shopify_app/blob/main/app/controllers/concerns/shopify_app/ensure_has_session.rb) controller concern will load a user session via `current_shopify_session`. As part of loading this session, this concern will also ensure that the user session has the appropriate scopes needed for the application. If the user isn't found or has fewer permitted scopes than are required, they will be prompted to authorize the application.
- [EnsureHasSession](https://github.com/Shopify/shopify_app/blob/main/app/controllers/concerns/shopify_app/ensure_has_session.rb) controller concern will load a user session via `current_shopify_session`. As part of loading this session, this concern will also ensure that the user session has the appropriate scopes needed for the application and that it is not expired (when `check_session_expiry_date` is enabled). If the user isn't found or has fewer permitted scopes than are required, they will be prompted to authorize the application.
- This controller concern should be used if you don't need your app to make calls on behalf of a user. With that in mind, there are a few other embedded concerns that are mixed in to ensure that embedding, CSRF, localization, and billing allow the action for the user.
- Example
```ruby
Expand Down Expand Up @@ -228,6 +234,9 @@ class User < ActiveRecord::Base
end
```

## Expiry date
When the configuration flag `check_session_expiry_date` is set to true, the user session expiry date will be checked to trigger a re-auth and get a fresh user token when it is expired. This requires the `ShopifyAPI::Auth::Session` `expires` attribute to be stored. When the `User` model includes the `UserSessionStorageWithScopes` concern, a DB migration can be generated with `rails generate shopify_app:user_model --skip` to add the `expires_at` attribute to the model.

## Migrating from shop-based to user-based token strategy

1. Run the `user_model` generator as [mentioned above](#user-online-token-storage).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddUserExpiresAtColumn < ActiveRecord::Migration[<%= rails_migration_version %>]
def change
add_column :users, :expires_at, :datetime
end
end
20 changes: 20 additions & 0 deletions lib/generators/shopify_app/user_model/user_model_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,26 @@ def create_scopes_storage_in_user_model
end
end

def create_expires_at_storage_in_user_model
expires_at_column_prompt = <<~PROMPT
It is highly recommended that apps record the User session expiry date. \
This will allow to check if the session has expired and re-authenticate \
without a first call to Shopify.
After running the migration, the `check_session_expiry_date` configuration can be enabled.
The following migration will add an `expires_at` column to the User model. \
Do you want to include this migration? [y/n]
PROMPT

if new_shopify_cli_app? || Rails.env.test? || yes?(expires_at_column_prompt)
migration_template(
"db/migrate/add_user_expires_at_column.erb",
"db/migrate/add_user_expires_at_column.rb",
)
end
end

def update_shopify_app_initializer
gsub_file("config/initializers/shopify_app.rb", "ShopifyApp::InMemoryUserSessionStore", "User")
end
Expand Down
8 changes: 8 additions & 0 deletions lib/shopify_app/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class Configuration
attr_accessor :api_version

attr_accessor :reauth_on_access_scope_changes
attr_accessor :check_session_expiry_date
attr_accessor :log_level

# customise urls
Expand All @@ -44,6 +45,9 @@ class Configuration
# takes a ShopifyApp::BillingConfiguration object
attr_accessor :billing

# Work in Progress: enables token exchange authentication flow
attr_accessor :wip_new_embedded_auth_strategy

def initialize
@root_url = "/"
@myshopify_domain = "myshopify.com"
Expand Down Expand Up @@ -118,6 +122,10 @@ def shop_access_scopes
def user_access_scopes
@user_access_scopes || scope
end

def use_new_embedded_auth_strategy?
wip_new_embedded_auth_strategy && embedded_app?
end
end

class BillingConfiguration
Expand Down
6 changes: 6 additions & 0 deletions lib/shopify_app/controller_concerns/login_protection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ def activate_shopify_session
return redirect_to_login
end

if ShopifyApp.configuration.check_session_expiry_date && current_shopify_session.expired?
ShopifyApp::Logger.debug("Session expired, redirecting to login")
clear_shopify_session
return redirect_to_login
end

if ShopifyApp.configuration.reauth_on_access_scope_changes &&
!ShopifyApp.configuration.user_access_scopes_strategy.covers_scopes?(current_shopify_session)
clear_shopify_session
Expand Down
17 changes: 12 additions & 5 deletions lib/shopify_app/session/session_repository.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ def retrieve_user_session_by_shopify_user_id(user_id)
user_storage.retrieve_by_shopify_user_id(user_id)
end

def destroy_shop_session_by_domain(shopify_domain)
shop_storage.destroy_by_shopify_domain(shopify_domain)
end

def destroy_user_session_by_shopify_user_id(user_id)
user_storage.destroy_by_shopify_user_id(user_id)
end

def store_shop_session(session)
shop_storage.store(session)
end
Expand Down Expand Up @@ -73,18 +81,17 @@ def load_session(id)
def delete_session(id)
match = id.match(/^offline_(.*)/)

record = if match
if match
domain = match[1]
ShopifyApp::Logger.debug("Destroying session by domain - domain: #{domain}")
Shop.find_by(shopify_domain: match[1])
destroy_shop_session_by_domain(domain)

else
shopify_user_id = id.split("_").last
ShopifyApp::Logger.debug("Destroying session by user - user_id: #{shopify_user_id}")
User.find_by(shopify_user_id: shopify_user_id)
destroy_user_session_by_shopify_user_id(shopify_user_id)
end

record.destroy

true
end

Expand Down
4 changes: 4 additions & 0 deletions lib/shopify_app/session/shop_session_storage.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ def retrieve_by_shopify_domain(domain)
construct_session(shop)
end

def destroy_by_shopify_domain(domain)
destroy_by(shopify_domain: domain)
end

private

def construct_session(shop)
Expand Down
4 changes: 4 additions & 0 deletions lib/shopify_app/session/shop_session_storage_with_scopes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ def retrieve_by_shopify_domain(domain)
construct_session(shop)
end

def destroy_by_shopify_domain(domain)
destroy_by(shopify_domain: domain)
end

private

def construct_session(shop)
Expand Down
4 changes: 4 additions & 0 deletions lib/shopify_app/session/user_session_storage.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ def retrieve_by_shopify_user_id(user_id)
construct_session(user)
end

def destroy_by_shopify_user_id(user_id)
destroy_by(shopify_user_id: user_id)
end

private

def construct_session(user)
Expand Down
25 changes: 25 additions & 0 deletions lib/shopify_app/session/user_session_storage_with_scopes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ def store(auth_session, user)
user.shopify_token = auth_session.access_token
user.shopify_domain = auth_session.shop
user.access_scopes = auth_session.scope.to_s
user.expires_at = auth_session.expires

user.save!
user.id
Expand All @@ -30,6 +31,10 @@ def retrieve_by_shopify_user_id(user_id)
construct_session(user)
end

def destroy_by_shopify_user_id(user_id)
destroy_by(shopify_user_id: user_id)
end

private

def construct_session(user)
Expand All @@ -52,6 +57,7 @@ def construct_session(user)
scope: user.access_scopes,
associated_user_scope: user.access_scopes,
associated_user: associated_user,
expires: user.expires_at,
)
end
end
Expand All @@ -67,5 +73,24 @@ def access_scopes
rescue NotImplementedError, NoMethodError
raise NotImplementedError, "#access_scopes= must be defined to hook into stored access scopes"
end

def expires_at=(expires_at)
super
rescue NotImplementedError, NoMethodError
if ShopifyApp.configuration.check_session_expiry_date
raise NotImplementedError,
"#expires_at= must be defined to handle storing the session expiry date"
end
end

def expires_at
super
rescue NotImplementedError, NoMethodError
if ShopifyApp.configuration.check_session_expiry_date
raise NotImplementedError, "#expires_at must be defined to check the session expiry date"
end

nil
end
end
end
2 changes: 1 addition & 1 deletion lib/shopify_app/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module ShopifyApp
VERSION = "21.9.0"
VERSION = "21.10.0"
end
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "shopify_app",
"version": "21.9.0",
"version": "21.10.0",
"repository": "git@github.com:Shopify/shopify_app.git",
"author": "Shopify",
"license": "MIT",
Expand Down
12 changes: 11 additions & 1 deletion test/generators/user_model_generator_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,14 @@ class UserModelGeneratorTest < Rails::Generators::TestCase
end
end

test "create User with access_scopes migration with --new-shopify-cli-app flag provided" do
test "create expires_at migration for User model" do
run_generator
assert_migration "db/migrate/add_user_expires_at_column.rb" do |migration|
assert_match "add_column :users, :expires_at, :datetime", migration
end
end

test "create User with all migrations with --new-shopify-cli-app flag provided" do
Rails.env = "mock_environment"

run_generator ["--new-shopify-cli-app"]
Expand All @@ -44,6 +51,9 @@ class UserModelGeneratorTest < Rails::Generators::TestCase
assert_migration "db/migrate/add_user_access_scopes_column.rb" do |migration|
assert_match "add_column :users, :access_scopes, :string", migration
end
assert_migration "db/migrate/add_user_expires_at_column.rb" do |migration|
assert_match "add_column :users, :expires_at, :datetime", migration
end
end

test "updates the shopify_app initializer to use User to store session" do
Expand Down
26 changes: 26 additions & 0 deletions test/shopify_app/configuration_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -228,4 +228,30 @@ class ConfigurationTest < ActiveSupport::TestCase
end
assert_equal "Invalid user access scopes strategy - expected a string", error.message
end

test "#use_new_embedded_auth_strategy? is true when wip_new_embedded_auth_strategy is on for embedded apps" do
ShopifyApp.configure do |config|
config.embedded_app = true
config.wip_new_embedded_auth_strategy = true
end

assert ShopifyApp.configuration.use_new_embedded_auth_strategy?
end

test "#use_new_embedded_auth_strategy? is false for non-embedded apps even if wip_new_embedded_auth_strategy is configured" do
ShopifyApp.configure do |config|
config.embedded_app = false
config.wip_new_embedded_auth_strategy = true
end

refute ShopifyApp.configuration.use_new_embedded_auth_strategy?
end

test "#use_new_embedded_auth_strategy? is false when wip_new_embedded_auth_strategy is off" do
ShopifyApp.configure do |config|
config.wip_new_embedded_auth_strategy = false
end

refute ShopifyApp.configuration.use_new_embedded_auth_strategy?
end
end
Loading

0 comments on commit 715ee97

Please sign in to comment.