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

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


※過去記事はこちら
[第1回【スマレジアプリを作ってみた#1】アプリの登録とPostman設定](https://qiita.com/JoBins/items/d485e1afa5687eef18ca)
[第2回【スマレジアプリを作ってみた#1】引換券モニターアプリの仕様と構成](https://qiita.com/JoBins/items/a450cd5b898c712dde0b)


# データベース構造

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

![Database Design For Queue Monitor.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/908996/0a489135-c8b7-76c0-6440-ead7269d4ce4.png)

# 認証フロー

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

まずは次の図で、このアプリに関する認証フローの概要を把握しましょう。
![Flowchart Qmonitor Authentication.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/908996/a1daffbc-8a77-85bb-030e-9ccb98c00934.png)

### 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にリダイレクトした後、ユーザーはスマレジにログインします。

![Screenshot from 2020-12-16 15-04-03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/908996/406339d1-6a4a-99ec-3650-7c4f5f040cba.png)

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

![認証ページ.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/908996/cbd0b0c5-381e-1b29-900c-655bc7f8a1be.jpeg)



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

**スコープ定義**

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

- `openid`
- `email`
- `offline_access`

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



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


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

![環境設定-Qmonitor-アプリ-スマレジ・デベロッパーズ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/908996/81379465-5d7d-61c1-100d-021ebdedfde2.png)

### 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_type`が`authorization_code`になっているかを確認してください。
※`client_id`と`client_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": "user@example.com",
    "contract": {
        "id": "sb_12a432",
        "is_owner": true
    }
}
```

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

上記のステップで取得した`contract_id`を使って、アプリアクセストークンを取得します。
そのために、`client_id`、` client_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_type`が`client_credentials`になっているかを確認してください。
※`client_id`と`client_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仕様書の[店舗一覧取得](https://www1.smaregi.dev/apidoc/#operation/getStores)を確認してください。


```
$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_id`と`smaregi_contract_id`は異なるという点に注意してください。
また、`smaregi_store_id`と`store_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エンドポイントについての詳細は[こちら](https://developers.smaregi.dev/apidoc/common/#section/API)でも確認できます。

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


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


#ソースコード

今回の記事に関するソースコードは[Github](https://github.com/JoBinsJP/smaregi-authentication)に載せております。
よかったらご利用ください！

---

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