
Auth0のIDトークンでMagentoのREST API呼び出しを認証・認可できるようにモジュールを自作してみた
最近Magentoと戯れているリテールアプリ共創部@大阪の岩田です。
タイトル通りですが、Magentoのモジュールを自作してREST API呼び出しをAuth0から発行されたIDトークンで認証・認可できるようにしてみました。以下に手順などをご紹介します。
環境
今回利用した環境は以下の通りです。
- PHP: 8.2.28
- Magento: 2.4.7-p4
- auth0-php: 8.13.0
モチベーション
MagentoのREST APIはアクセストークンによる認証に対応していますが、このアクセストークンはMagentoから発行されたアクセストークンである必要があります。例えば/rest/V1/customers/meというエンドポイントにGETリクエストを送信して自身の顧客情報を取得する場合、手順は以下のようになります。
- アクセストークンの取得
curl 'http://localhost/rest/V1/integration/customer/token' \
--header 'Content-Type: application/json' \
--data-raw '{"username":"<ユーザー名>","password":"<パスワード>"}'
- 取得したアクセストークンをリクエストヘッダにセットしてGET
/rest/V1/customers/meを実行
curl 'http://localhost/rest/V1/customers/me \
-H 'Authorization: Bearer <取得したアクセストークン>'
しかしMagentoに独自カスタマイズを加え、認証にAuth0を利用している場合はAuth0から発行されたトークンを利用してMagentoのREST APIを呼び出したくなるでしょう。これを実現するために独自モジュールを作ってみました。
前提
前提として以下のような形式のIDトークンを使って認証・認可することとします。
{
"http://localhost/user_metadata": {
"magento_customer_id": 1
},
"nickname": "iwata.tomoya",
"name": "iwata-example@example.com",
"picture": "https://s.gravatar.com/avatar/119659c28d16f22d01eb48a6f3ee1391?s=480&r=pg&d=https%3A%2F%2Fcdn.auth0.com%2Favatars%2Fiw.png",
"updated_at": "2025-04-08T02:07:44.428Z",
"email": "iwata-example@example.com",
"email_verified": true,
"iss": "https://<Auth0のテナントID>.auth0.com/",
"aud": "qWs...",
"sub": "auth0|6265...",
"iat": 1744078065,
"exp": 1745374065,
"sid": "xHt...",
"nonce": "eTh..."
}
Auth0のユーザーメタデータと、Actionsを使ってhttp://localhost/user_metadataというネームスペース配下にmagento_customer_idという項目を追加しています。この項目はその名の通りMagento側のDBで保持している顧客のIDです。
また、以後で詳解する実装はあくまで検証目的となっており、異常系の考慮など色々と手抜きをしています。もし実際に同様の実装を利用したい場合は適宜チェック処理など追加して下さい。
実装
今回実装したモジュールの構造は以下の通りです。本来はAuth0を使った認証・認可周りだけ別モジュールに切り出して、REST APIを実装するモジュールから参照するような構成が望ましいですが、検証目的なので全部まとめて1つのモジュールとしています。
app/code/
└── CmIwata
└── Customer
├── Api
│ ├── Auth0JwtUserContextInterface.php
│ ├── Auth0JwtUserTokenReaderInterface.php
│ └── Auth0JwtUserTokenValidatorInterface.php
├── Controller
│ └── Rest
│ └── ParamOverriderAuth0.php
├── Model
│ ├── Auth0Authorization.php
│ ├── Auth0JwtTokenExpirationValidator.php
│ ├── Auth0JwtUserContext.php
│ └── Auth0JwtUserTokenReader.php
├── composer.json
├── etc
│ ├── module.xml
│ ├── webapi.xml
│ └── webapi_rest
│ └── di.xml
└── registration.php
1つずつ詳細を見ていきましょう。
モジュールの設定など
まず registration.php です。特に特別なことはしておらず。モジュールの名前を定義しているぐらいです。
<?php
use Magento\Framework\Component\ComponentRegistrar;
ComponentRegistrar::register(ComponentRegistrar::MODULE, 'CmIwata_Customer', __DIR__);
composer.jsonです。Auth0関連の処理を実装するためにauth0/auth0-phpを導入しています。
{
"name": "cm-iwata/customer",
"version": "1.0.0",
"description": "N/A",
"type": "magento2-module",
"license": [
"Proprietary"
],
"require": {
"auth0/auth0-php": "^8.13.0"
},
"autoload": {
"files": [
"registration.php"
],
"psr-4": {
"CmIwata\\Customer\\": ""
}
}
}
続いて etc/module.xmlです。こちらも特に変わったことはしておらず、依存しているMagentoのモジュールを列挙しているぐらいです。
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
<module name="CmIwata_Customer">
<sequence>
<module name="Magento_Authorization"/>
<module name="Magento_Customer"/>
<module name="Magento_Integration"/>
<module name="Magento_Webapi"/>
<module name="Magento_JwtUserToken"/>
<module name="Magento_JwtUserToken"/>
</sequence>
</module>
</config>
etc/webapi_rest/di.xmlです。Magentoの色々なインターフェースに対して独自モジュールで追加する具象クラスをDIするよう設定しています。Auth0関連の設定も手抜きしてここに記述しているので、流用する際はご注意下さい。
<?xml version="1.0"?>
<!--
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-->
<?xml version="1.0"?>
<!--
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<preference for="CmIwata\Customer\Api\Auth0JwtUserContextInterface" type="CmIwata\Customer\Model\Auth0JwtUserContext"/>
<preference for="CmIwata\Customer\Api\Auth0JwtUserTokenReaderInterface" type="CmIwata\Customer\Model\Auth0JwtUserTokenReader" />
<preference for="CmIwata\Customer\Api\Auth0JwtUserTokenValidatorInterface" type="CmIwata\Customer\Model\Auth0JwtTokenExpirationValidator" />
<preference for="Magento\Customer\Api\CustomerRepositoryInterface"
type="Magento\Customer\Model\ResourceModel\CustomerRepository" />
<type name="Magento\Webapi\Controller\Rest\ParamsOverrider">
<arguments>
<argument name="paramOverriders" xsi:type="array">
<item name="%magento_customer_id%" xsi:type="object">
CmIwata\Customer\Controller\Rest\ParamOverriderAuth0
</item>
</argument>
</arguments>
</type>
<type name="CmIwata\Customer\Controller\Rest\ParamOverriderAuth0">
<arguments>
<argument name="userContext" xsi:type="object">CmIwata\Customer\Model\Auth0JwtUserContext</argument>
</arguments>
</type>
<type name="Magento\Customer\Model\Customer\AuthorizationComposite">
<arguments>
<argument name="authorizationChecks" xsi:type="array">
<item name="rest_customer_authorization" xsi:type="object">CmIwata\Customer\Model\Auth0Authorization</item>
</argument>
</arguments>
</type>
<type name="Auth0\SDK\Configuration\SdkConfiguration">
<arguments>
<argument name="audience" xsi:type="array">
<item name="0" xsi:type="string">http://localhost</item>
</argument>
<argument name="domain" xsi:type="string">https://<Auth0のテナントID>.auth0.com/</argument>
<argument name="clientId" xsi:type="string">Auth0のクライアントID</argument>
<argument name="clientSecret" xsi:type="string">Auth0のクライアントシークレット</argument>
<argument name="tokenAlgorithm" xsi:type="string">RS256</argument>
<argument name="cookieSecret" xsi:type="string">hogefugapiyo</argument>
</arguments>
</type>
</config>
REST APIの仕様
続いてREST APIの仕様を定義します。今回はMagento標準のGET /rest/V1/customers/meと類似の処理を実行するGET /rest/V1/customers/auth0-meというエンドポイントを追加してみます。
etc/webapi.xmlの定義は以下の通りです。
<?xml version="1.0"?>
<!--
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
-->
<routes xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Webapi:etc/webapi.xsd">
<route url="/V1/customers/auth0-me" method="GET">
<service class="Magento\Customer\Api\CustomerRepositoryInterface" method="getById"/>
<resources>
<resource ref="auth0_self"/>
</resources>
<data>
<parameter name="customerId" force="false">%magento_customer_id%</parameter>
</data>
</route>
</routes>
処理自体はMagento標準のGET /rest/V1/customers/meと同様にCustomerRepositoryInterfaceのgetByIdを呼び出すよう設定しています。getByIdの引数であるcustomerIdはリクエストパラメータから%magento_customer_id%というパラメータを動的に取得&設定します。このパラメータを取得するロジックは後ほど実装します。
<resource ref="auth0_self"/>の部分ではMagento標準で定義されているselfは利用せず、独自のauth0_selfというリソースを利用しています。後ほど実装するPHP側の処理に関連しますが、Auth0から取得した妥当なIDトークンは付与されている場合のみAPIの呼び出しを許可します。
PHPコードの実装
ここからはPHPコードを実装していきます。
まずAuth0JwtUserContextInterfaceというインターフェースを定義しました。
<?php
namespace CmIwata\Customer\Api;
interface Auth0JwtUserContextInterface
{
public function getMagentoCustomerId(): ?int;
}
Magento標準のUserContextInterfaceだとgetUserIdとgetUserTypeを実装する必要があるのですが、今回はユーザーの種別に関わらずAPIの呼び出しを許可したいため独自インターフェースを定義する方針としました。
この修正に合わせてさらに独自のインターフェースを追加します。
次はAuth0JwtUserTokenReaderInterface というインターフェースです。
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);
namespace CmIwata\Customer\Api;
use Magento\JwtUserToken\Model\Data\JwtTokenData;
interface Auth0JwtUserTokenReaderInterface
{
public function read(string $token): JwtTokenData;
}
Magento標準のUserTokenReaderInterfaceだとUserContextInterfaceを実装したUserTokenを返却する必要があり、UserTokenのコンストラクタにはUserContextInterfaceを渡す必要があるためです。
同様にAuth0JwtUserTokenValidatorInterfaceというインターフェースも定義します。
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);
namespace CmIwata\Customer\Api;
use Magento\JwtUserToken\Model\Data\JwtTokenData;
interface Auth0JwtUserTokenValidatorInterface
{
public function validate(JwtTokenData $token): void;
}
Magento標準のUserTokenValidatorInterfaceだとvalidateの引数にUserTokenを渡す必要があるためです。
続いてAuth0JwtUserContextInterfaceを実装するAuth0JwtUserContextの実装です。
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace CmIwata\Customer\Model;
use CmIwata\Customer\Api\Auth0JwtUserContextInterface;
use CmIwata\Customer\Api\Auth0JwtUserTokenReaderInterface;
use CmIwata\Customer\Api\Auth0JwtUserTokenValidatorInterface;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\App\Request\Http;
use Magento\Framework\Exception\AuthorizationException;
use Magento\Framework\Jwt\Payload\ClaimsPayloadInterface;
use Magento\Framework\ObjectManager\ResetAfterRequestInterface;
use Magento\Integration\Api\Exception\UserTokenException;
class Auth0JwtUserContext implements ResetAfterRequestInterface, Auth0JwtUserContextInterface
{
protected Http $request;
protected bool $isRequestProcessed = false;
private readonly Auth0JwtUserTokenReaderInterface $userTokenReader;
private readonly Auth0JwtUserTokenValidatorInterface $userTokenValidator;
protected readonly ClaimsPayloadInterface $claims;
public function __construct(
\Magento\Framework\App\RequestInterface $request,
?Auth0JwtUserTokenReaderInterface $tokenReader = null,
?Auth0JwtUserTokenValidatorInterface $tokenValidator = null
) {
$this->request = $request;
$this->userTokenReader = $tokenReader ?? ObjectManager::getInstance()->get(Auth0JwtUserTokenReaderInterface::class);
$this->userTokenValidator = $tokenValidator
?? ObjectManager::getInstance()->get(Auth0JwtUserTokenValidatorInterface::class);
}
public function getMagentoCustomerId():?int
{
$this->processRequest();
if (!isset($this->claims)) {
return null;
}
return $this->claims->getClaims()['http://localhost/user_metadata']->getValue()['magento_customer_id'];
}
public function _resetState(): void
{
$this->isRequestProcessed = null;
$this->claims = null;
}
protected function processRequest(): void
{
if ($this->isRequestProcessed) {
return;
}
$authorizationHeaderValue = $this->request->getHeader('Authorization');
if (!$authorizationHeaderValue) {
$this->isRequestProcessed = true;
return;
}
$headerPieces = explode(" ", $authorizationHeaderValue);
if (count($headerPieces) !== 2) {
$this->isRequestProcessed = true;
return;
}
$tokenType = strtolower($headerPieces[0]);
if ($tokenType !== 'bearer') {
$this->isRequestProcessed = true;
return;
}
$bearerToken = $headerPieces[1];
try {
$token = $this->userTokenReader->read($bearerToken);
} catch (UserTokenException $exception) {
$this->isRequestProcessed = true;
return;
}
try {
$this->userTokenValidator->validate($token);
} catch (AuthorizationException $exception) {
$this->isRequestProcessed = true;
return;
}
$this->claims = $token->getJwtClaims();
$this->isRequestProcessed = true;
}
}
色々と書いていますが、基本的にMagento\Webapi\Model\Authorization\TokenUserContextの実装を流用しています。UserContextInterfaceの代わりにAuth0JwtUserContextInterfaceを実装するためにgetMagentoCustomerIdというメソッドを実装し、関連する箇所を調整しています。
public function getMagentoCustomerId():?int
{
$this->processRequest();
if (!isset($this->claims)) {
return null;
}
return $this->claims->getClaims()['http://localhost/user_metadata']->getValue()['magento_customer_id'];
}
getMagentoCustomerIdの処理ではIDトークンからmagento_customer_idを取り出して返却しています。
processRequestの中ではUserTokenReaderInterfaceのreadとUserTokenValidatorInterfaceのvalidateでトークンの読み込みと検証を行います。Auth0のIDトークンに対応させるため、それぞれAuth0JwtUserTokenReaderとAuth0JwtTokenExpirationValidatorを独自実装します。
Auth0のIDトークンを読み込むためのAuth0JwtUserTokenReaderの実装です。
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);
namespace CmIwata\Customer\Model;
use Auth0\SDK\Configuration\SdkConfiguration;
use Auth0\SDK\Exception\InvalidTokenException;
use Auth0\SDK\Token as Auth0Token;
use CmIwata\Customer\Api\Auth0JwtUserTokenReaderInterface;
use Magento\Framework\Jwt\Claim\Audience;
use Magento\Framework\Jwt\Claim\IssuedAt;
use Magento\Framework\Jwt\Claim\Issuer;
use Magento\Framework\Jwt\Claim\PrivateClaim;
use Magento\Framework\Jwt\Claim\Subject;
use Magento\Framework\Jwt\Payload\ClaimsPayload;
use Magento\JwtUserToken\Model\Data\Header;
use Magento\JwtUserToken\Model\Data\JwtTokenData;
class Auth0JwtUserTokenReader implements Auth0JwtUserTokenReaderInterface
{
private SdkConfiguration $sdkConfiguration;
public function __construct(SdkConfiguration $sdkConfiguration)
{
$this->sdkConfiguration = $sdkConfiguration;
}
public function read(string $token): JwtTokenData
{
try {
$auth0Token = new Auth0Token($this->sdkConfiguration, $token, Auth0Token::TYPE_ID_TOKEN);
$auth0Token->verify();
} catch (InvalidTokenException $exception) {
throw new UserTokenException('Failed to read JWT token', $exception);
}
$arrToken = $auth0Token->toArray();
$iat = \DateTimeImmutable::createFromFormat('U', (string) $auth0Token->getIssued());
$exp = \DateTimeImmutable::createFromFormat('U', (string) $auth0Token->getExpiration());
$namespace = 'http://localhost/user_metadata';
$uid = $arrToken[$namespace]['magento_customer_id'];
$claim = new ClaimsPayload(
[
new Audience($auth0Token->getAudience()),
new Subject($auth0Token->getSubject()),
new PrivateClaim(
$namespace,
$arrToken[$namespace]
),
new IssuedAt($iat),
new Issuer($auth0Token->getIssuer())
]
);
// TODO 今回Headerは利用しないので空配列で返却している
return new JwtTokenData($iat, $exp, new Header([]), $claim);
}
}
Auth0のSDKを利用し、IDトークンをパースしてUserTokenを返却しています。今回の実装は手抜き実装になっているので以下の点に注意してください。
- Auth0の公開鍵をキャッシュしていない
Headerに空配列を返却しているClaimsPayloadにIDトークンの全項目を含めていない
このクラスはdi.xmlの
<preference for="CmIwata\Customer\Api\Auth0JwtUserTokenReaderInterface" type="CmIwata\Customer\Model\Auth0JwtUserTokenReader" />
という記述によってAuth0JwtUserTokenReaderInterfaceにDIされます。
次はAuth0のIDトークンを検証するAuth0JwtTokenExpirationValidatorの実装です。
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);
namespace CmIwata\Customer\Model;
use CmIwata\Customer\Api\Auth0JwtUserTokenValidatorInterface;
use Magento\Framework\Exception\AuthorizationException;
use Magento\Framework\Stdlib\DateTime\DateTime as DtUtil;
use Magento\JwtUserToken\Model\Data\JwtTokenData;
class Auth0JwtTokenExpirationValidator implements Auth0JwtUserTokenValidatorInterface
{
private DtUtil $datetimeUtil;
public function __construct(DtUtil $datetimeUtil)
{
$this->datetimeUtil = $datetimeUtil;
}
public function validate(JwtTokenData $token): void
{
if ($this->isTokenExpired($token)) {
throw new AuthorizationException(__('Token has expired'));
}
}
private function isTokenExpired(JwtTokenData $token): bool
{
return $token->getExpires()->getTimestamp() <= $this->datetimeUtil->gmtTimestamp();
}
}
トークンの有効期限をチェックするだけのシンプルな実装です。
このクラスはdi.xml以下の記述によってAuth0JwtUserTokenValidatorInterfaceにDIされます。
<preference for="CmIwata\Customer\Api\Auth0JwtUserTokenValidatorInterface" type="CmIwata\Customer\Model\Auth0JwtTokenExpirationValidator" />
もう少しです...次はAuthorizationInterfaceを実装するAuth0Authorizationという独自のクラスを定義します。AuthorizationInterfaceはisAllowedの実装が必要なので、ここに独自の認可ロジックを記述します。
<?php
/**
*
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);
namespace CmIwata\Customer\Model;
use CmIwata\Customer\Api\Auth0JwtUserContextInterface;
use Magento\Framework\AuthorizationInterface;
class Auth0Authorization implements AuthorizationInterface
{
private Auth0JwtUserContextInterface $userContext;
public function __construct(
Auth0JwtUserContextInterface $userContext,
) {
$this->userContext = $userContext;
}
public function isAllowed($resource, $privilege = null): bool
{
if ($resource === 'auth0_self'
&& $this->userContext->getMagentoCustomerId()
) {
return true;
}
return false;
}
}
今回は認可の条件として以下の2点をチェックしています。
- 対象APIに
auth0_selfリソースからの呼び出し許可が設定されているか Auth0JwtUserContextInterfaceのgetMagentoCustomerIdの結果がTruthyか
このクラスはdi.xmlの以下の記述によってAuthorizationCompositeのauthorizationChecksにDIされます。
<type name="Magento\Customer\Model\Customer\AuthorizationComposite">
<arguments>
<argument name="authorizationChecks" xsi:type="array">
<item name="rest_customer_authorization" xsi:type="object">CmIwata\Customer\Model\Auth0Authorization</item>
</argument>
</arguments>
</type>
最後にParamOverriderAuth0というクラスを実装します。
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace CmIwata\Customer\Controller\Rest;
use CmIwata\Customer\Api\Auth0JwtUserContextInterface;
use Magento\Framework\Webapi\Rest\Request\ParamOverriderInterface;
class ParamOverriderAuth0 implements ParamOverriderInterface
{
private Auth0JwtUserContextInterface $aws0JwtUserContext;
public function __construct(Auth0JwtUserContextInterface $aws0JwtUserContext)
{
$this->aws0JwtUserContext = $aws0JwtUserContext;
}
public function getOverriddenValue():int
{
return $this->aws0JwtUserContext->getMagentoCustomerId();
}
}
di.xmlの以下の記述によってwebapi.xmlに定義した%magento_customer_id%を解決するためにこのクラスが利用されます。
<type name="Magento\Webapi\Controller\Rest\ParamsOverrider">
<arguments>
<argument name="paramOverriders" xsi:type="array">
<item name="%magento_customer_id%" xsi:type="object">
CmIwata\Customer\Controller\Rest\ParamOverriderAuth0
</item>
</argument>
</arguments>
</type>
実際の処理はgetOverriddenValueに記述されていて、Auth0JwtUserContextInterfaceのgetMagentoCustomerIdによって値が解決されます。
色々実装してきましたが、これでようやく準備完了です!
やってみる
ここまでで準備ができたので./bin/magento module:enable CmIwata_Customerでモジュールを有効化、./bin/magento cache:flushでキャッシュをフラッシュしてからcurlコマンでREST APIの呼び出しを試してみます。
curl 'http://localhost/rest/V1/customers/auth0-me' \
-H 'Authorization: Bearer <Auth0から取得したIDトークン>'
実行結果は以下のようになりました。
{
"id": 1,
"group_id": 1,
"default_billing": "1",
"default_shipping": "1",
"created_at": "2025-04-04 00:40:36",
"updated_at": "2025-04-04 00:41:41",
"created_in": "Default Store View",
"email": "iwata-example.@example.com",
"firstname": "Tomoya",
"lastname": "Iwata",
"store_id": 1,
"website_id": 1,
"addresses": [
{
"id": 1,
"customer_id": 1,
"region": {
"region_code": "大阪",
"region": "大阪",
"region_id": 0
},
"region_id": 0,
"country_id": "JP",
"street": [
"その辺"
],
"company": "クラスメソッド",
"telephone": "0120-1234-5678",
"postcode": "5410044",
"city": "大阪",
"firstname": "智哉",
"lastname": "岩田",
"default_shipping": true,
"default_billing": true
}
],
"disable_auto_group_change": 0,
"extension_attributes": {
"is_subscribed": false
}
}
無事に GET /rest/V1/customers/me相当のレスポンスが返却されてきました!
まとめ
今回はCustomerRepositoryInterfaceのgetByIdを呼び出すことで GET /rest/V1/customers/me相当の処理を実装しましたが、独自のインターフェースを定義してgetByAuth0Idのようなメソッドを実装したり、Auth0JwtUserContextInterfaceを拡張してgetAuth0Idのようなメソッドを実装するとできることの幅が色々と広がりそうです。
今回実装したサンプルは以下のリポジトリで公開しているので必要に応じてご参照ください。







