
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
のようなメソッドを実装するとできることの幅が色々と広がりそうです。
今回実装したサンプルは以下のリポジトリで公開しているので必要に応じてご参照ください。