Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## Unreleased
- [Minor] Add support for the OAuth token exchange grant (`OAuth::tokenExchange`) and expiring offline access tokens with refresh support (`OAuth::refreshAccessToken`), `Session::getRefreshToken`/`getRefreshTokenExpiresAt`, and an `expiringOfflineAccessToken` option on `OAuth::callback`. See [#462](https://github.com/Shopify/shopify-api-php/issues/462).

## v6.1.1 - 2026-03-02
- [#456](https://github.com/Shopify/shopify-api-php/pull/456) [Patch] Update firebase/php-jwt to ^7.0 to address security vulnerability (GHSA-2x45-7fc3-mxwq)
Expand Down
39 changes: 38 additions & 1 deletion docs/usage/oauth.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,44 @@ To do that, you can call the `Shopify\Auth\OAuth::callback` method in the endpoi
| `cookies` | `array` | Yes | - | HTTP request cookies, from which the OAuth session will be loaded. This must be a hash of `cookie name => value` pairs. The value will be cast to string so they may be objects that implement `toString`. |
| `query` | `array` | Yes | - | The HTTP request URL query values. |
| `setCookieFunction` | `callable` | No | - | An override function to set cookies in the HTTP request. In order to be framework-agnostic, the built-in `setcookie` method is applied. If that method does not work for your chosen framework, a function that sets cookies can be passed in. |
| `expiringOfflineAccessToken` | `bool` | No | `false` | Whether to request an [expiring offline access token](https://shopify.dev/docs/apps/build/authentication-authorization/access-tokens/offline-access-tokens) (with a refresh token), instead of a non-expiring one. Has no effect for online sessions. |

If successful, this method will return a `Session` object, which is described [below](#the-session-object). Once the session is created, you can use [utility methods](./utils.md) to fetch it.

## Token exchange

If your app is [embedded](https://shopify.dev/docs/apps/build/authentication-authorization/access-tokens/authorization-code-grant) and rendered in the Shopify Admin, you can use the [token exchange grant](https://shopify.dev/docs/apps/build/authentication-authorization/access-tokens/token-exchange) to obtain an access token directly from the session token that App Bridge provides, without redirecting the merchant through the authorization code grant flow described above.

Call `Shopify\Auth\OAuth::tokenExchange`, which takes in the following arguments:

| Parameter | Type | Required? | Default Value | Notes |
| --- | --- | :---: | :---: | --- |
| `shop` | `string` | Yes | - | A Shopify domain name or hostname. |
| `sessionToken` | `string` | Yes | - | The session token (Shopify id token) provided by App Bridge, taken from the `Authorization` header or the `id_token` URL param. |
| `requestedTokenType` | `string` | Yes | - | The type of token to request, one of the `Shopify\Auth\RequestedTokenType::ONLINE_ACCESS_TOKEN` or `Shopify\Auth\RequestedTokenType::OFFLINE_ACCESS_TOKEN` constants. |
| `expiring` | `bool` | No | `false` | Whether the requested offline access token should be an expiring token with a refresh token. Has no effect when requesting an online access token. |

```php
$session = Shopify\Auth\OAuth::tokenExchange(
$shop,
$sessionToken,
Shopify\Auth\RequestedTokenType::OFFLINE_ACCESS_TOKEN,
expiring: true,
);
```

This method stores and returns the resulting `Session`, just like `OAuth::callback` does.

## Refreshing expiring offline access tokens

Expiring offline access tokens come with a refresh token that can be used to obtain a new access token without merchant interaction. When an offline session has a refresh token (see the `Session` fields [below](#the-session-object)), call `Shopify\Auth\OAuth::refreshAccessToken` with that session to get a new one:

```php
$newSession = Shopify\Auth\OAuth::refreshAccessToken($session);
```

Shopify rotates the refresh token every time it's used, so make sure to persist the `Session` returned by this method; the previous refresh token becomes unusable immediately. If the refresh token has expired (after 90 days of inactivity), or the session provided is an online session, this method throws an `InvalidArgumentException` or `HttpRequestException`, and you'll need to re-authenticate the merchant.

## The `Session` object

The OAuth process will create a new `Session` object and store it in your `Context::$SESSION_STORAGE`. This object is a collection of data that is needed to authenticate requests to Shopify, so you can access shop data using the Admin API.
Expand All @@ -56,9 +91,11 @@ The `Session` object provides the following methods to expose its data:
| `getShop` | `string` | The shop to which the session belongs. |
| `getState` | `string` | The `state` of the session. This is mainly used for OAuth. |
| `getScope` | `string \| null` | The effective API scopes enabled for this session. |
| `getExpires` | `DateTime \| null` | The expiration date of the session, or null if it is offline. |
| `getExpires` | `DateTime \| null` | The expiration date of the session, or null if it doesn't expire. |
| `isOnline` | `bool` | Whether the session is [online or offline](https://shopify.dev/docs/apps/auth#api-access-modes). |
| `getAccessToken` | `string \| null` | The Admin API access token for the session. |
| `getOnlineAccessInfo` | `AccessTokenOnlineUserInfo \| null` | The data for the user associated with this session. Only applies to online sessions. |
| `getRefreshToken` | `string \| null` | The refresh token for this session, if it's an expiring offline access token. |
| `getRefreshTokenExpiresAt` | `DateTime \| null` | The expiration date of the refresh token, if this session has one. |

[Back to guide index](../README.md)
45 changes: 44 additions & 1 deletion src/Auth/AccessTokenResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,24 @@ class AccessTokenResponse
protected string $accessToken;
protected string $scope;

// Note: this is intentionally not named `expiresIn` to avoid clashing with the property of the same name
// declared on AccessTokenOnlineResponse, which has a different (non-nullable) type.
private readonly ?int $tokenExpiresIn;
private readonly ?string $refreshToken;
private readonly ?int $refreshTokenExpiresIn;

public function __construct(
string $accessToken,
string $scope
string $scope,
?int $expiresIn = null,
?string $refreshToken = null,
?int $refreshTokenExpiresIn = null
) {
$this->accessToken = $accessToken;
$this->scope = $scope;
$this->tokenExpiresIn = $expiresIn;
$this->refreshToken = $refreshToken;
$this->refreshTokenExpiresIn = $refreshTokenExpiresIn;
}

public function getAccessToken(): string
Expand All @@ -26,4 +38,35 @@ public function getScope(): string
{
return $this->scope;
}

/**
* The number of seconds until the access token expires. Only present for expiring offline access tokens.
*
* @return int|null
*/
public function getExpiresIn(): ?int
{
return $this->tokenExpiresIn;
}

/**
* The refresh token that can be used to obtain a new access token. Only present for expiring offline access
* tokens.
*
* @return string|null
*/
public function getRefreshToken(): ?string
{
return $this->refreshToken;
}

/**
* The number of seconds until the refresh token expires. Only present for expiring offline access tokens.
*
* @return int|null
*/
public function getRefreshTokenExpiresIn(): ?int
{
return $this->refreshTokenExpiresIn;
}
}
Loading
Loading