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
35 changes: 0 additions & 35 deletions .github/workflows/gardener-notify-event.yml

This file was deleted.

116 changes: 0 additions & 116 deletions .github/workflows/gardener-notify-slack.yml

This file was deleted.

16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [1.1.0]

- Add optional `client_assertion` keyword argument to `exchange_using_client_credentials` and `exchange_using_client_credentials_async`. Pass an app-created JWT to authenticate one request with a client assertion; omit it, or pass `None`, to keep client-secret authentication.
- Client-credentials exchanges now reject redirects for both authentication methods, including with an injected client that enables redirects. This changes client-secret calls that relied on redirect following. Client defaults and other operations are unchanged.
- Add `shopify_app.assertions.create_client_assertion`, a standalone signer for one client assertion. It takes an already-loaded RSA (`RS256`) or EC P-256 (`ES256`) private key, and needs no `ShopifyApp` instance, client secret or network access.
- Add the optional `assertions` extra for signing support: `pip install "shopifyapp[assertions]"`. Base dependencies are unchanged.
- Redact `client_assertion` in logged request bodies.

## [1.0.3]

- Return a 401 response instead of raising `TypeError` when the HMAC header of `verify_webhook_req` or `verify_flow_action_req`, or the `signature` of `verify_app_proxy_req`, contains characters that are not ASCII.
- Validate the shop before building request URLs. `exchange_using_client_credentials`, `refresh_token_exchanged_access_token` and `admin_graphql_request` return an error when `shop` is not a valid shop name (e.g., `shop-name`). `exchange_using_token_exchange` builds the request URL from the validated shop name.
- `app_home_patch_id_token` returns a 400 response when the `shop` query parameter is not a valid shop domain (e.g., `shop-name.myshopify.com`).

## [1.0.2]

- Harden App Home Redirect URL handling.
Expand Down
106 changes: 103 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ This package encodes the secure, correct way to build a Shopify app. Most of wha
- [Verifying requests with exchangeable ID tokens](#verifying-requests-with-exchangeable-id-tokens)
- [Navigating inside and outside the App Home iframe](#navigating-inside-and-outside-the-app-home-iframe)
- [GraphQL requests](#graphql-requests)
- [Optional access scopes](#optional-access-scopes)
- [Verifying requests without exchangeable ID tokens](#verifying-requests-without-exchangeable-id-tokens)
- [Getting access tokens with client credentials](#getting-access-tokens-with-client-credentials)
- [Authenticating client credentials with a client assertion](#authenticating-client-credentials-with-a-client-assertion)
- [Async variants](#async-variants)
- [Common misuses to avoid](#common-misuses-to-avoid)
- [Contributing, issues, feedback and feature requests](#contributing-issues-feedback-and-feature-requests)
Expand All @@ -45,11 +47,20 @@ Before you start, make sure you have:
pip install --upgrade shopifyapp
```

To sign client assertions locally with `create_client_assertion`, install the
optional signing support as well:

```bash
pip install --upgrade "shopifyapp[assertions]"
```

## Requirements

- Python >= 3.8
- httpx for making HTTP requests
- pyjwt for JWT token handling
- The `assertions` extra adds pyjwt's cryptography backend. It is only needed
for `create_client_assertion`; every other feature works without it.

## Features

Expand All @@ -69,9 +80,13 @@ Request Verification:
Exchange:

- `exchange_using_token_exchange`: Use Token Exchange to exchange an ID token for an access token
- `exchange_using_client_credentials`: Get access tokens via client credentials
- `exchange_using_client_credentials`: Get access tokens via client credentials, authenticated with the client secret or with a client assertion
- `refresh_token_exchanged_access_token`: Refresh an access token that was created using Token Exchange. Checks if a token refresh can and should happen.

Signing:

- `create_client_assertion`: Create one signed JWT to authenticate a client credentials request. Standalone; needs the `assertions` extra.

GraphQL:

- `admin_graphql_request`: Make Admin API GraphQL requests with automatic retry handling
Expand Down Expand Up @@ -767,6 +782,14 @@ def process_job(shop):
- `data`: The GraphQL response data (dict), or `None` if the request failed.
- `extensions`: The GraphQL extensions (dict, e.g., cost information), or `None` if not present.

### Optional access scopes

Use [`admin_graphql_request`](#graphql-requests) on your server to query granted scopes or revoke optional scopes. Check the revoke mutation's `userErrors` even when the package result is `ok`.

To request merchant approval, use App Bridge in embedded apps or Shopify's approval redirect in standalone apps.

See [Manage access scopes](https://shopify.dev/docs/apps/build/authentication-authorization/manage-access-scopes) for configuration, deployment, examples, and grant rules.

### Verifying requests without exchangeable ID tokens

The following requests do not provide the required information for token exchange:
Expand Down Expand Up @@ -867,10 +890,87 @@ The `access_token` dataclass contains:
| `access_mode` | Always "offline" |
| `token` | The access token string |
| `scope` | The granted scopes |
| `expires` | ISO 8601 datetime when the token expires (24 hours) |
| `expires` | ISO 8601 expiry calculated from the endpoint response |
| `user` | Always `None` for client credentials |

Note: Client credentials tokens expire after 24 hours and do not include a refresh token. When the token expires, request a new one using `exchange_using_client_credentials` with the same credentials.
Note: Client credentials tokens do not include a refresh token. Read `expires`
to know when the token expires, and request a new one with
`exchange_using_client_credentials` when it does.

These methods do not follow redirects with either authentication method, even
if an injected client enables them. A 307/308 can forward the credential-bearing
POST body. Redirects return a failed result using the existing error-code rules,
with the original status in the HTTP log. The per-request setting leaves the
client configuration and other operations unchanged.

> **Upgrade note.** Client-secret requests now override redirect-enabled client
> defaults. Check the shop and intended endpoint instead of relying on a redirect.

### Authenticating client credentials with a client assertion

Instead of the client secret, one client credentials call can authenticate with
a client assertion: a short-lived JWT your app signs with a private key whose
public key is registered with Shopify. Assertion authentication needs an active
registered public key and an app installation on the target shop. To create a key pair and register its public key, see
[Get application-only access tokens](https://shopify.dev/docs/apps/build/authentication-authorization/application-only-access-tokens).

`client_assertion` is a keyword argument of each call, not app configuration.
Omit it, or pass `None`, to keep client-secret authentication. The app
constructor is unchanged, and the package never signs an assertion for you.
Keep the real client secret in the existing app configuration.

Supply an already-loaded native private key to `create_client_assertion`, then
pass the returned string to one exchange call:

```python
from shopify_app.assertions import create_client_assertion

assertion = create_client_assertion(
shop="shop-name",
client_id=client_id,
kid="the-registered-key-id",
private_key=private_key,
lifetime_seconds=60, # optional: default 60, maximum 300
)

result = shopify.exchange_using_client_credentials(
shop="shop-name", client_assertion=assertion
)
```

`create_client_assertion` is standalone: it needs no `ShopifyApp` instance,
client secret, ID token or network access. It accepts only an already-loaded
RSA private key of at least 2048 bits (`RS256`) or an EC private key on the
P-256 curve (`ES256`). PEM strings, JWKs, file paths, passphrases, shared
secrets, public keys and signer callbacks are rejected. Invalid input raises
`ClientAssertionArgumentError`, a missing `assertions` extra raises
`ClientAssertionSigningUnavailableError`, and a crypto-library failure raises
`ClientAssertionSigningError`. No failure makes an HTTP request.

Load the key once, for example with `cryptography`'s
`serialization.load_pem_private_key`, and pass the key object, never the key
text. The package rejects key text without echoing it, but an error reporter
that records local variables (Sentry's Python SDK does by default) can still
capture text passed by mistake.

An assertion authenticates only once:

- Create a fresh assertion for every attempt, including after a timeout, a 503
or any other failure, and even while the previous assertion is still inside
its lifetime.
- An assertion request makes one HTTP attempt and never falls back to the
client secret. Redirects are not followed on either authentication path.
- The request must carry no other credential. If an injected HTTP client is
configured with an `Authorization` or `X-Shopify-Access-Token` header, the
call returns a `configuration_error` result before sending anything.
- An injected client must be an inspectable `httpx.Client` or `AsyncClient`
without default query parameters. The package does not change client defaults.
Custom hooks and transports must honor the per-request authentication and
redirect controls and must not add retries.
- The logged request body shows `client_assertion` as `[REDACTED]`.

If you sign with a remote signer or a KMS, skip `create_client_assertion` and
pass your own completed assertion to `client_assertion`.

### Async variants

Expand Down
8 changes: 8 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ dependencies = [
"httpx>=0.24.0",
]

[project.optional-dependencies]
# Asymmetric signing support for shopify_app.assertions.create_client_assertion.
# Keep this optional: apps that authenticate with a client secret, or that sign
# with a remote signer or KMS, do not need a local crypto backend.
assertions = [
"pyjwt[crypto]>=2.8.0",
]

[project.urls]
homepage = "https://github.com/Shopify/shopify-app-python/"
issues = "https://community.shopify.dev/c/shopify-cli-libraries/14"
Expand Down
Loading
Loading