【スマレジアプリを作ってみた#3】認証機能の実装


さて、スマレジのデータを利用したアプリの開発について、
第3回目はスマレジ引換券モニターアプリの認証機能の実装についてお届けしたいと思います!
アプリ開発の中でも特につまづきやすいところかと思います!
注意点などもまとめておりますでぜひ参考にしてみてください!

※過去記事はこちら
第1回【スマレジアプリを作ってみた#1】アプリの登録とPostman設定
第2回【スマレジアプリを作ってみた#1】引換券モニターアプリの仕様と構成

データベース構造

まず最初に、認証機能の実装に必要なデータベース構造について見ていきましょう。
ER図のように、ユーザー情報とログイン情報を保存するためのauth_usersテーブルと 、
スマレジ契約情報を保存するためのsmaregi_contractsテーブルがあります。
smaregi_contractsauth_usersは1対多のリレーションシップとなります。

認証フロー

スマレジAPIを介してアプリがユーザーのデータにアクセスするには、
アプリアクセストークンを取得する必要があります。
この記事全体を通して、アクセストークンを取得する方法と、
アクセストークンを取得するために必要な手順等について説明します。

まずは次の図で、このアプリに関する認証フローの概要を把握しましょう。

1. 認可リクエスト

OAuth 2.0 Authorization Code Grant と OpenID Connect に基づいて、
スマレジアカウントでのログインを利用できます。
ユーザーが初めてアプリを開く際は、認可エンドポイントURLに
クエリパラメータを含めてユーザーをリダイレクトします。
ユーザーは認証ページでスマレジアカウントを使ってアプリにアクセスを許可します。

認可エンドポイントURL

https://id.smaregi.dev/authorize
//ユーザーをスマレジ認可エンドポイントURLにリダイレクトする
return redirect()->to(authorizeUser());

public function authorizeUser(): string
{
    return "https://id.smaregi.dev/authorize?response_type=code&client_id={your_client_id}}&scope=openid+email+offline_access";
}

authorize()関数は
https://id.smaregi.dev/authorize?response_type=code&client_id= {your_client_id}}&scope = openid + email + offline_access
を返します。
このURLにリダイレクトした後、ユーザーはスマレジにログインします。

ログイン後、次の認証ページが表示されます。

パラメーター 値の説明
response_type code
client_id アプリのクライアントID
scope ユーザーに認可を要求するスコープ。複数の場合は半角スペースで結合して指定。

スコープ定義

スコープは、アプリによるユーザーデータへのアクセスを制限するために使用されます。
このアプリでは、OpenIDConnectで指定された

  • openid
  • email
  • offline_access

のスコープを使用して、APIプラットフォームでアクセスを取得します。

スコープ 説明
openid ログインユーザーの識別子の取得、UserInfoエンドポイントへのアクセス。
email ログインユーザーのアカウントのメールアドレスを参照。openidの指定が必須。
offline_access リフレッシュトークンの取得。

スマレジはクレデンシャルを確認します。
ユーザーが有効な場合、authorization_code
スマレジアプリで定義されたリダイレクトURIを介してcodeとしてアプリに送信されます。

2.authorization_codeを使ってアクセストークンをリクエストする

authorization_codeを受信した後、
アプリは再度スマレジにユーザーアクセストークンの取得をリクエストします。

ユーザーアクセストークンエンドポイントURL

https://id.smaregi.dev/authorize/token

前のステップで受け取った authorization_codeを使用してアクセストークンを取得しましょう。

[
    'access_token'  => $accessToken,
] = $this->authenticate($authorizationCode); // 前のステップで取得した認可コード


public function authenticate(string $authorizationCode): array
{
    $url          = "https://id.smaregi.dev/authorize/token";
    $clientId     = {client_id};//アプリのclient_id
    $clientSecret = {client_secret}; //アプリのclient_secret

    $option = new HttpRequestOption();
    $option->url($url);
    $option->method('POST');
    $option->token(sprintf("Basic %s", base64_encode("$clientId:$clientSecret")));

    $formData               = [];
    $formData['grant_type'] = 'authorization_code';
    $formData['code']       = $authorizationCode;
    $formData['scope']      = 'pos.transactions:write pos.stores:read pos.products:read';

    $option->formData($formData);
    return $this->request($option);
}

grant_typeauthorization_codeになっているかを確認してください。
client_idclient_secretをコロン(:)で結合して
 Base64エンコードしたものを指定しているかを確認してください。

レスポンス例

{
    "token_type": "Bearer",
    "expires_in": 3600,
    "access_token": "xNjA3NTc2MDIzLCJuYmYiOjE2MDc1NzYwMj...",
    "scope": "pos.transactions:write pos.stores:read pos.products:read"
}

3. アクセストークンを使用してユーザー情報を取得する

前のステップで取得したアクセストークンを使って、次のエンドポイントを介して
ユーザーのcontract_idおよびその他のユーザー情報を要求します。

UserInfoエンドポイントURL

https://id.smaregi.dev/userinfo

スマレジユーザー情報と契約情報を取得する


public function getUserInfo(string $accessToken): array
{
    $url    = "https://id.smaregi.dev/userinfo";
    $option = new HttpRequestOption();
    $option->url($url);
    $option->token($accessToken);
    return $this->request($option);
}
[   'sub'      => $smaregiId,
    'email'    => $smaregiEmail,
    'contract' => $smaregiContract,
] = $this->getUserInfo($accessToken);

contract_idを取得する

$contractId = Arr::get($smaregiContract, 'id');

レスポンス例

{
    "sub": "smaregi:qmonitorX1Y3Z",
    "email": "[email protected]",
    "contract": {
        "id": "sb_12a432",
        "is_owner": true
    }
}

4.contract_idを使ってアプリアクセストークンを取得する

上記のステップで取得したcontract_idを使って、アプリアクセストークンを取得します。
そのために、client_idclient_secret、およびその他の必要なスコープを提供する必要があります。

アプリアクセストークンエンドポイントURL

https://id.smaregi.dev/app/{contract_id}/token
['access_token' => $appAccessToken] = $this->appAccessToken($contractId);


public function appAccessToken(string $contractId): array
{
    $url          = "https://id.smaregi.dev/app/{contract_id}/token"; // 前のステップで取得したcontract_id
    $clientId     = {client_id}; //アプリのclient_id
    $clientSecret = {client_secret}; //アプリのclient_secret

    $option = new HttpRequestOption();
    $option->url($url);
    $option->method('POST');
    $option->token(sprintf("Basic %s", base64_encode("$clientId:$clientSecret")));

    $formData               = [];
    $formData['grant_type'] = 'client_credentials';
    $formData['scope']      = 'pos.transactions:write pos.transactions:read pos.stores:read pos.products:read';

    $option->formData($formData);
    return $this->request($option);
}

grant_typeclient_credentialsになっているかを確認してください。
client_idclient_secretをコロン(:)で結合して
 Base64エンコードしたものを指定しているかを確認してください。

このトークンを使って、アプリがユーザーの代わりにスマレジシステムにアクセスすることができます。

レスポンス例

{
    "scope": "pos.transactions:write pos.stores:read pos.products:read",
    "token_type": "Bearer",
    "expires_in": 3600,
    "access_token": "FrGbxdfNjA3aNTc2MDIzLCJuYmYiOjE2MDc1NzYwMj..."
}

5. ユーザー情報を保存してアプリにログインする

最後に、ユーザー情報をアプリに保存・登録します。
このプロセスでは、ユーザーの契約情報も保存します。

スマレジ契約情報を保存する

public function saveContract(string $contractId, string $appAccessToken): SmaregiContract
{
    return SmaregiContract::updateOrCreate(
        ['contract_id' => $contractId],
        [
            'smaregi_system_access_token' => $appAccessToken,
        ]
    );
}
$contract = $this->saveContract($contractId, $appAccessToken);

ユーザー情報を保存してログインする

public function login(SmaregiContract $smaregiContract, $userData)
{
    $user = $smaregiContract->users()->updateOrCreate(
        [
            'smaregi_contract_id' => $smaregiContract->id,
            'email'       => $userData->email,
        ],
        [
            'smaregi_id'            => $userData->smaregiId,
            'email'                 => $userData->email,
            'smaregi_access_token'  => $userData->smaregiAccessToken,
            'smaregi_refresh_token' => $userData->smaregiRefreshToken,
            'logged_in_at'          => Carbon::now(),
        ]
    );
    auth()->login($user, true);
}
$userData = [
    'smaregiId'           => $smaregiId,
    'smaregiEmail'        => $smaregiEmail,
    'smaregiAccessToken'  => $accessToken,
    'smaregiRefreshToken' => $refreshToken,
]

$this->login($contract, $userData);

店舗情報を取得する
アクセストークンを使って店舗情報を取得するため、次のエンドポイントにリクエストを送信します。
https://api.smaregi.dev/{contract_id}/pos/stores/
詳細はスマレジ・プラットフォームAPI POS仕様書の店舗一覧取得を確認してください。

$stores = getStores($appAccessToken, $contract->contract_id);
public function getStores(string $accessToken, string $contractId, array $query = []): array
{
    $url    = "https://api.smaregi.dev/{$contractId}/pos/stores/";

    $option = new HttpRequestOption();
    $option->url($url);
    $option->token("Bearer $accessToken");
    $option->query($query);

    return $this->request($option);
}

それから、取得した契約IDに紐づく店舗情報をアプリに保存します。

$this->saveStores($contract, $stores);
public function saveStores(SmaregiContract $contract, $store)
{
    return $contract->smaregi_stores()->updateOrCreate(
        [
            'smaregi_contract_id'      => $contract->id,
            'smaregi_store_id'         => $store->storeId,
        ],
        [
            'smaregi_store_id'         => $store->storeId,
            'smaregi_store_name'       => $store->storeName,
            'is_paused'                => $store->isPaused,
        ]
    );
}

注意点

上記のコードを見るとわかるように、店舗情報を取得するには
アクセストークンとcontract_idを使用しますが、
このアプリに保存するには、引換券モニターDBで作成された
smaregi_contract_idを使用する必要があります。
したがって、contract_idsmaregi_contract_idは異なるという点に注意してください。
また、smaregi_store_idstore_idも異なるため注意してください。

店舗情報を更新するときはいつでも、次のようにしてください。

Store::updateOrCreate(
    [
        'id' => $store->id,
    ],
    [
        'smaregi_store_name' => $data->smaregi_store_name
    ]
);

次のようにしないよう注意してください。
こうすると違う契約IDの情報で更新してしまいます。

Store::updateOrCreate(
    [
        'smaregi_store_id' => $store->smaregi_store_id], 
    [
        'smaregi_store_name' => $data->smaregi_store_name
    ]
);

APIエンドポイントについての詳細はこちらでも確認できます。

エンドポイントURLは、アプリの開発環境によって異なります。
この記事では、Sandbox環境について例を記載しています。

Sandbox環境 本番環境
アクセストークンAPI https://id.smaregi.dev https://id.smaregi.jp
プラットフォームAPI https://api.smaregi.dev https://api.smaregi.jp

ソースコード

今回の記事に関するソースコードはGithubに載せております。
よかったらご利用ください!


第3回目は認証機能の実装についてお送りしました。
次回はアプリの実装についてお届けします!