カラーミーショップ Nuxt OpenID Connect

カラーミーショップ アプリストアの認証をOpenID Connectで再設計する

カラーミーショップ Nuxt OpenID Connect

2022年11月下旬から、カラーミーショップ アプリストアのショップオーナー認証では、カラーミーショップのIDプロバイダ (IdP)が持つOpenID Connect (OIDC)の機能を利用する構成に切り替えました。この変更によってログインセッションの管理をIdPに一元化し、ショップオーナーがカラーミーショップ アプリストアとアプリストア アプリをよりスムーズに利用できるようになりました。

この記事では、Nuxtを利用したWebアプリケーションであるカラーミーショップ アプリストアでIdPとOIDCを利用するに至った経緯とその設計を紹介します。

カラーミーショップ アプリストアとは

カラーミーショップ アプリストア(以降、アプリストアとします)は、アプリストア アプリを通じてカラーミーショップの4万店以上のショップに追加機能を提供するためのプラットフォームです。アプリストアを利用するのはショップオーナーです。

アプリストアは次の技術でなりたっています。

  • フロントエンドはユニバーサルモードで動くNuxt 2アプリケーション
  • バックエンドはRuby on RailsによるREST APIサーバ

カラーミーショップでは、以前からIdPを通じてOAuth 2.0の認可サーバの機能を提供しています1。アプリストア アプリ(以降、アプリとします)は、その認可サーバを通じて、認可コードフローに基づき認可されるOAuthクライアントです。アプリそれぞれが独自のWebアプリケーションとして提供されています。

従来のアプリストアでの認証・認可

従来のアプリストアでの認証・認可について一言で述べると「アプリストアとアプリの両方で認証情報を入力する必要がある」というものでした。

アプリストアの認証

従来のアプリストアでは、バックエンドに独自の認証エンドポイントを設け、ショップオーナーのログイン時にフロントエンドから認証情報を渡すことで、ショップオーナーを認証していました。この認証機構はサービス中の他のシステムとはDBを共有していることを除いて独立しており、ログインセッションも独自に管理していました。

IdPの認証、アプリの認可

これまでは、ショップオーナーがアプリを利用するには次の手順を踏んでいました。

  • アプリストアにログインする
    • 2要素認証を利用していれば、ワンタイムパスワードも入力する
  • アプリストアを経由して、アプリを利用するため認可エンドポイントに遷移する
  • IdPへのログインを要求されるので、ショップオーナーがログインする
    • 2要素認証を利用していれば、ワンタイムパスワードも入力する
  • あらためて認可エンドポイントに遷移し、アプリストア上でインストールしたアプリストア アプリを認可する

このショップオーナーの操作の流れをシーケンス図で表現すると次のようになります。

従来のアプリ利用フロー

「アプリストアの認証」で述べたとおり、IdPのログインセッションはアプリストアとは独立していました。

従来の方法の問題点

上述した手順のとおり、各サービスを利用するときに都度認証し直していただく必要がある点が課題でした。サービス提供開始時の事情により、アプリストアとアプリで別々の認証機構を持っており、結果として認証に手間がかかる状態になっていました。本来は、各サービスの認証機構をカラーミーショップ全体として1つのIdPに集約し、IdPのログインセッションが発行済みなら各サービスで認証し直す必要はない、という状態がユーザー体験上は理想的です。

また、このような認証の不便さはセキュリティ上の問題にもつながると考えられます。カラーミーショップでは2要素認証の利用を推奨していますが、上述した問題から2要素認証を使うとログインに必要な作業がさらに多くなるという問題を抱えていました。あまりにも利用者に強いる手間が多いと2要素認証の利用を避けられてしまい、結果として、2要素認証を提供している意味が薄れてしまうおそれがあります。

解決策

上述した問題を解決するために、アプリストアでショップオーナーを認証するときに、IdPに実装したOpenID Connect (OIDC)のOpenID Provider (OP)を使うようにします。

IdPはOAuth 2.0の認可サーバを提供していると述べましたが、サービスの内部システムでは、そのサーバをOPとすることでOIDCの機能も提供しています。iOS/Android用アプリはRelying Party (RP)としてこのOPをすでに利用していました。今回はアプリストアもそのOPを通じてショップオーナーを認証する構成に切り替えることで、認証機構を外部に委譲し、ログインセッションをIdPで一元管理します。

カラーミーショップにおいてIdPとOIDCを使った認証を使っていく利点として、主に次の項目があります。

  • OIDC自体がID連携における標準的な仕様であり、適切に実装することでセキュアな認証機能を提供できる
  • OIDCを提供するIdPにシステム全体の認証に関するロジックを集約できる
    • セキュリティ対応のコストが減る、修正漏れなどを防げる
  • IdPでログインセッションを一元管理するので、ショップオーナーが都度認証し直す必要なくシステム内の複数のサービスを利用できる仕組みが自然に実装できる
  • 将来的にショップオーナーが利用できるサービスを増やすときに認証機能を個別に実装する必要がなくなり、サービスの提供を早められるので、ビジネス的にもよい影響がある

認証機構の設計

解決策を実現するためにどのような方法をとったかについて説明します。

OIDCを用いたアプリストアへのログイン

Nuxtの場合、nuxt/authがOIDCを使うためのモジュールとしてまず選択肢に挙がります。しかし、nuxt/authのOIDCスキームは、いまのところSPA側で認証を完結させるものになっています2。nuxt/authのOIDCスキームでOPに送る必要があるのはクライアントIDだけで、クライアントシークレットを送る必要がありません。言い換えると、nuxt/authではOAuthクライアントがconfidentialではなくpublicであることを前提としています。

アプリストアのNuxtアプリケーションをpublicなOAuthクライアントと見なしてnuxt/authを使うという手もあります。一方で、nuxt/authを使うとSPAがアクセストークンやIDトークンを取得する構造になります。この構造だと、次の点でアタックサーフェスを増やすことになるので、その対応に注意する必要が出てきます。

  • SPAに脆弱性があるときに、攻撃者にトークンを取得される可能性がある
  • ブラウザの利用者がアクセストークンを閲覧できるので、呼び出してほしくないREST APIの呼び出しを実行される可能性がある

この点を考慮して、Nuxtをユニバーサルモードで動かしていることを活かすという方法が考えられます。つまり、Nuxtのサーバサイドで認可コードの送信とトークンの取得、検証を実行すると言うことです。これにより、アプリストアをconfidentialなOAuthクライアントと見なすことができます。

また、この方法だと、トークンをNuxtのサーバサイドだけで取り扱えばよく、ブラウザに不要なトークンを渡す必要がありません。アプリストアとしての認証を完了するときは、Nuxtのサーバサイドからブラウザに対してアプリ固有のログインセッションを発行するだけで済むようになります。

以上を踏まえて、次のような設計にしました。

  • NuxtのサーバサイドにOIDCログインに関するパラメータを取得するためのAPIを追加する
    • OIDCのディスカバリエンドポイントから情報を取得し、セキュリティパラメータと合わせてアプリにあった構造のJSONをブラウザに返す
    • Nuxt 2なのでserver middlewareを使ってAPIを追加した
  • OPからのコールバックをNuxtのサーバサイドで受けて、ブラウザにレスポンスを返す前に認可コードとトークンの交換、IDトークンの検証を実行する
    • コールバックを受けてのサーバサイドでのロジック実行でもserver middlewareを利用した
  • IDトークンの検証まで終わったら、アプリストアとしてのセッションをCookieで発行し、ブラウザにログイン完了をレスポンスする

Nuxt 2でサーバサイドにserver middlewareでAPIを追加するときは次のようなイメージです。

// server_middleware/auth-api.ts
import express, { Request, Response } from 'express';

const app = express();
app.get('/config', async (req: Request, res: Response) => {
  // 認可エンドポイントに遷移するための情報を返す
});

export default app;
// nuxt.config.js
// /auth にAPIをマウントし GET /auth/config エンドポイントを追加する例
serverMiddleware: [
  { path: '/auth', handler: '~/server_middleware/auth-api' },
]

ログイン時のリクエスト、レスポンスのシーケンス図は次のとおりです。「Nuxt SPA」と「Nuxtサーバサイド」がアプリストアを構成しています。

OIDCを用いたアプリストアへのログインのシーケンス図

OIDC RP-Initiated Logoutを用いたアプリストアからのログアウト

OIDCのログアウトには、次のようにいくつか種類があります。

  • RP-Initiated Logout
    • RPからエンドユーザがOPをログアウトするようにリクエストする。ログアウト後にRPにリダイレクトさせることもできる
  • Back-Channel Logout
    • OPからログイン済みPRに通信してエンドユーザをRPからログアウトさせる
  • Front-Channel Logout
    • OPの画面でブラウザなどを経由してログイン済みRPに通信しエンドユーザをRPからログアウトさせる

今回は、カラーミーショップでは今後RP化できるサービスを含めてもそこまでサービスの数は多くないことから、シンプルさをとってRP-Initiated Logoutを選択しました。RPであるアプリストアからログアウトする際に、まずOPに遷移してOPからログアウトし、その後リダイレクトを経てRPでもログアウトします。

RP-Initiated Logoutの仕様上"RECOMMENDED"になっているid_token_hintは利用しませんでした。これは、カラーミーショップで複数アカウントでのログインをサポートしないので、IDトークンのクレームまで確認してログアウトする利用者を区別する必要が今のところないことが理由です。

ログアウト時のリクエスト、レスポンスのシーケンス図は次のとおりです。「Nuxt SPA」と「Nuxtサーバサイド」がアプリストアを構成しています。

RP-Initiated Logoutを用いたIdPとアプリストアからのログアウトのシーケンス図

アプリストア アプリへのログインの簡略化

ショップオーナーはOAuth 2.0の認可コードフローに基づいてアプリを認可し、利用します。また、ショップオーナーがアプリを使うときはアプリストア内の導線から利用開始します。このとき、「従来のアプリストアでの認証・認可」で述べたとおり、認証情報を複数回入力する必要があるという問題がありました。

アプリストアでIdPを使うことでこの問題も解決できます。ショップオーナーのログインセッションを一元管理するIdPはOAuthの認可サーバであり、またOIDCのOPとしての機能も持っています。そこで、アプリストアのログインでIdPのログインセッションを発行したあとに、ショップオーナーがOAuthクライアントを認可するときは、意図したアカウントかどうかの確認だけをしてから認可同意画面に遷移するようにしました。

OAuthクライアント認可前のアカウント確認画面

OIDCでは、エンドユーザに認証するアカウントの選択を促すために、認可エンドポイントにpromptというパラメータの値としてselect_accountを送ることができます。今回設けたアカウント確認画面は、promptパラメータとしてselect_accountを利用したとみなして、つねに単一のアカウントを選択させる挙動に固定しているといえます。複数アカウントでのログインをサポートしていないことから、現状はこの実装にしています。

以上の方法により、アプリストアから各アプリに遷移するとき利用者が毎回認証情報を入れる必要がなくなり、アプリストアからアプリを利用しやすくなりました。

まとめ

この記事では、カラーミーショップの既存のサービスであるアプリストアをOIDCのRPになるように変更し、アプリストアとアプリが使うIdPへの認証ロジックの集約を進めることで、結果的にユーザー体験向上につなげた事例を紹介しました。OAuthやOIDCが提供する仕様に基づいて、既存サービスの問題点を抽出し改善を加えていくことで、ユーザー体験を向上し、認証ロジック集約によるサービス自体のメンテナンス容易性向上も実現しました。

このようなWebアプリケーションの基盤の開発に興味があるかたは、ぜひ応募ページからご連絡ください。

謝辞

カラーミーショップのIdPの開発にあたっては、YAuth.jpのNov Matake (nov)さんにご助言をいただいております。また、本記事もnovさんにレビューいただきました。