<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:webfeeds="http://webfeeds.org/rss/1.0">
  <title>認証認可 カテゴリ | フューチャー技術ブログ</title>
  <subtitle>認証認可 カテゴリの記事一覧</subtitle>
  <icon>https://future-architect.github.io/feed_icon.png</icon>
  <logo>https://future-architect.github.io/apple-touch-icon.png</logo>
  <webfeeds:icon>https://future-architect.github.io/apple-touch-icon.png</webfeeds:icon>
  <webfeeds:accentColor>258fb8</webfeeds:accentColor>
  <link href="https://future-architect.github.io/categories/%E8%AA%8D%E8%A8%BC%E8%AA%8D%E5%8F%AF/atom.xml" rel="self"/>
  <link href="https://future-architect.github.io/categories/%E8%AA%8D%E8%A8%BC%E8%AA%8D%E5%8F%AF/"/>
  <updated>2025-12-04T15:00:00.000Z</updated>
  <id>https://future-architect.github.io/categories/%E8%AA%8D%E8%A8%BC%E8%AA%8D%E5%8F%AF/</id>
  <generator uri="https://hexo.io/">Hexo</generator>
  <entry>
    <title>Software Design 2025年12月号 「今さら聞けないID管理 認証基盤を構築する際に知っておくべきこと」に寄稿しました</title>
    <link href="https://future-architect.github.io/articles/20251205a/"/>
    <id>https://future-architect.github.io/articles/20251205a/</id>
    <published>2025-12-04T15:00:00.000Z</published>
    <updated>2025-12-04T15:00:00.000Z</updated>
    <author><name>藤井亮佑</name></author>
    <content type="html"><![CDATA[
<img fetchpriority="high" src="/images/2025/20251205a/image.png" alt="" width="400" height="565">


<h2 id="はじめに">はじめに</h2><p>こんにちは。TIG-DX の藤井です。</p>
<p>Software Design 2025 年 12 月号 第 1 特集の「今さら聞けない ID 管理 認証基盤を構築する際に知っておくべきこと」に、宮崎さん、市川さん、私の 3 名で寄稿をさせていただきました。</p>
<p>第3章「IDaaS とはどんなサービスか Auth0 から学ぶ機能と選定の勘所」を担当し、Auth0 の機能や活用事例、IDaaS を選定する際の基準等、実際に IDaaS を活用する際に有用な知見を詰め込みました。</p>
<p>本記事にて、簡単に宣伝させていただければと思います。</p>
<h2 id="Software-Design-とは">Software Design とは</h2><p>技術評論社より月刊で刊行されているOS・Web・プログラミング、技術者のスキルアップのための技術情報雑誌です。当社からも様々な分野についての特集で何度か寄稿をしています。</p>
<h2 id="今回の特集について">今回の特集について</h2><h3 id="そもそも-ID-管理とは">そもそも ID 管理とは</h3><p>世の中には数多くの Web サービスが存在し、その多くがアカウントを作成、ログインすることで利用できるようになっています。企業や学校などに所属している場合も、固有のアカウントが発行され、何らかのサービスを利用することがあると思います。こういったアカウントは、適切にユーザを識別し、ユーザに応じて適切に権限管理するために用いられています。このような仕組みのことを、一般に「ID 管理」と呼び、世にあるサービスの裏側では当たり前のように ID 管理が行われています。</p>
<p>当然のように存在し運用されているのが ID 管理という仕組みです。しかし、その仕組みの裏側は、そう単純ではありません。先述したユーザの識別と適切な権限管理、すなわち認証認可が正常に行えなければ、健全なサービス提供は不可能です。</p>
<p>さらに、ID 管理が行われているサービスを利用する場合、メールアドレスなどのユーザを識別する情報に加え、パスワードをシステムに登録することが多いはずです。これらの情報を紛失・流出させてしまう等があると、信用の失墜・訴訟など、大きな問題に発展してしまうため、適切かつ厳密に管理する必要があります（実際に個人情報が流出してしまったり、悪意ある攻撃者により窃取されてしまった…といった事例は枚挙に暇がありません）。</p>
<p>一方で、ID 管理はサービス提供へ密接に関わる仕組みです。そのため、ユーザにとって煩雑すぎたり、パフォーマンスが不十分だったり、システムの他領域との連携が困難だったりすると、サービスの品質低下にもつながります。</p>
<h3 id="ID-管理を学ぶきっかけとしての本特集">ID 管理を学ぶきっかけとしての本特集</h3><p>しかしながら、「なんとなくは理解しているが、具体的にどのような仕組みなのか？」「実際構築になるとどうすれば良いのか？」 「フルスクラッチ？ SaaS？ SaaS なら何を使えば良い？」など、より実践的な内容は実は知らない、という方は案外多いかと思います。</p>
<p>ID 管理を伴うサービスを構築・運用している場合であっても、ID 管理に関しては専門家のようなメンバーが存在し、その人依存になってしまっている、というようなこともあるのではないでしょうか。</p>
<p>今回の特集は、まさにそういった方に向けた内容となっています。当然ながら、この特集を一読すれば ID 管理についてすべて理解できる、構築・運用ができるようになる、といったわけではありません。より詳細な技術要素を調べ・学ぶ必要はありますし、継続的に知識をアップデートしていく必要もあります。ただし、その取っ掛かりになる、「詳しい人に聞きたいけど、今更こんなに基本的なことは聞けないなぁ…」といった知識は十二分に得られる内容となっています。</p>
<p>特に、当社が寄稿した第 3 章では、昨今広く利用されている ID 管理のための SaaS、IDaaS である Auth0 について取り上げています。</p>
<p>どういった機能が存在し、どのようなサービスが実現できるのか。少し汎化して、IDaaS を比較する際の観点は何か。といった、IDaaS を活用していくための、実践的な内容を記載しています。ID 管理するにあたって、Auth0 をはじめとする IDaaS を選択肢の1つとして取り入れることができる内容となっているかと思いますので、ぜひご一読いただければと思います。</p>
<h2 id="おわりに">おわりに</h2><p>先述の通り、ID 管理は昨今のサービス提供には欠かせないものである一方、意外と詳しい人は少ない、というのが実情だと認識しています。今回の特集を機に、より多くの方が ID 管理の世界に手を伸ばしていただき、より安心・安全・便利なサービスが増えることを願っています。</p>
<p>また、普段から ID 管理には携わっており、Auth0 も活用していますが、今回の寄稿をきっかけに改めて知識を整理・更新できました。雑誌に寄稿するという経験を得られたことも大変ありがたく思っております。このような機会をくださった技術評論社様に、この場を借りてお礼申し上げます。</p>
]]></content>
    <summary type="html">Software Design 2025 年 12 月号特集の「今さら聞けない ID 管理 認証基盤を構築する際に知っておくべきこと」に、宮崎さん、市川さん、私の 3 名で寄稿をさせていただきました。</summary>
    <category term="認証認可" scheme="https://future-architect.github.io/categories/%E8%AA%8D%E8%A8%BC%E8%AA%8D%E5%8F%AF/"/>
    <category term="Auth0" scheme="https://future-architect.github.io/tags/Auth0/"/>
    <category term="SoftwareDesign" scheme="https://future-architect.github.io/tags/SoftwareDesign/"/>
    <category term="出版" scheme="https://future-architect.github.io/tags/%E5%87%BA%E7%89%88/"/>
  </entry>
  <entry>
    <title>OpenAPIでOAuth認可周りのサーバサイドコード生成を比較</title>
    <link href="https://future-architect.github.io/articles/20240829a/"/>
    <id>https://future-architect.github.io/articles/20240829a/</id>
    <published>2024-08-28T15:00:00.000Z</published>
    <updated>2024-08-28T15:00:00.000Z</updated>
    <author><name>真野隼記</name></author>
    <content type="html"><![CDATA[<p>夏の自由研究連載2024 の3日目です。</p>
<h2 id="はじめに">はじめに</h2><p>TIG 真野です。</p>
<p>夏といえばコード生成というわけで、HTTP API仕様を定義するOpenAPIの <strong>security schemes</strong>（認証認可を定義するための箇所）で、Bearer／OAuth2／OpenID Connect (OIDC) 認証を設定すると、各コードジェネレータはどういったコード生成をしてくれるかを調べました。</p>
<p>なお、OpenAPIについてはOpenAPI Specification v3.0.3のコーディング規約を公開しましたの記事も参考ください。</p>
<p>この記事で利用した <code>openapi.yaml</code> や生成したサーバサイドのコードは以下のリポジトリにまとめています。</p>
<ul>
<li>https://github.com/ma91n/summer2024</li>
</ul>
<h2 id="コード生成の対象">コード生成の対象</h2><ul>
<li>Go言語のみ</li>
<li>サーバサイドのみ（※クライアントコードは対象外）</li>
</ul>
<h2 id="比較ツール">比較ツール</h2><p>Goコードを生成可能な以下のツールを対象にします。今どきはOpen API 3.0.3（3.1.0） を使うと思うので、3系に対応しているツールを選定しています。フューチャー技術ブログではgo-swagger記事がいくつかありますが、go-swaggerは2系にしか対応していないので対象外としています。</p>
<ol>
<li>ogen v1.3.0</li>
<li>oapi-codegen v2.2.0 <code>net/http</code> モード、<code>strict-server</code> モード</li>
<li>openapi-generator v7.8.0 <code>gorilla/mux</code> ルーターモード</li>
</ol>
<h2 id="検証の構成やコードについて">検証の構成やコードについて</h2><p>構成ですが、クライアントを <code>curl</code> で、JWTトークンをGo製のCLIツールで作成し、OAuth 2.0でいう認可サーバを無くした状態で検証しています（※本来は、公開鍵を <code>/oauth2/jwks</code> や <code>jwks_uri</code> で指定されたURLから取得できるようにすべきですが、ハードコードで省略しています）。</p>
<img fetchpriority="high" src="/images/2024/20240829a/openapi.drawio_(2).png" alt="openapi.drawio_(2).png" width="1200" height="719">

<h2 id="利用するJWTトークンについて">利用するJWTトークンについて</h2><p>認可サーバやIdPを今回用意しないので、替わりに <code>openssl</code> で秘密鍵・公開鍵を作成します。秘密鍵でJWTを署名し、公開鍵で検証することを想定しているので、非対称鍵系署名アルゴリズムを選定します。</p>
<p>詳しくない領域なので間違っていたらご指摘いただきたいですが、RS256は避けた方が良い というAuthleteさんの話を読み、今回はES512を利用することにします。</p>
<div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-162sgu1-1" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-162sgu1-1" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="comment"># 秘密鍵を生成</span></span><br><span class="line">openssl ecparam -name secp521r1 -genkey -noout -out ecprivatekey.pem</span><br><span class="line"></span><br><span class="line"><span class="comment"># 公開鍵を生成</span></span><br><span class="line">openssl ec -<span class="keyword">in</span> ecprivatekey.pem -pubout -out ecpubkey.pem</span><br></pre></td></tr></table></figure></div>

<p>秘密鍵を利用してJWTトークンを生成します。iss、subなど属性は適当にしています。スコープ（<code>scp</code>）は検証に用いたい値です。</p>
<div class="code-block"><figure class="highlight go"><input type="checkbox" id="code-wrap-162sgu1-2" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-162sgu1-2" title="コードの折り返しを切り替える"></label><figcaption><span>main.go</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">	_ <span class="string">&quot;embed&quot;</span></span><br><span class="line"></span><br><span class="line">	<span class="string">&quot;crypto/x509&quot;</span></span><br><span class="line">	<span class="string">&quot;encoding/pem&quot;</span></span><br><span class="line">	<span class="string">&quot;fmt&quot;</span></span><br><span class="line">	<span class="string">&quot;log&quot;</span></span><br><span class="line"></span><br><span class="line">	<span class="string">&quot;github.com/golang-jwt/jwt/v5&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment">//go:embed ecprivatekey.pem</span></span><br><span class="line"><span class="keyword">var</span> privateKey []<span class="type">byte</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">	<span class="comment">// 参考: https://stackoverflow.com/questions/21322182/how-to-store-ecdsa-private-key-in-go</span></span><br><span class="line">	block, _ := pem.Decode(privateKey)</span><br><span class="line"></span><br><span class="line">	ecPrivateKey, err := x509.ParseECPrivateKey(block.Bytes)</span><br><span class="line">	<span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">		log.Fatal(err)</span><br><span class="line">	&#125;</span><br><span class="line"></span><br><span class="line">	s, err := jwt.NewWithClaims(jwt.SigningMethodES512,</span><br><span class="line">		jwt.MapClaims&#123;</span><br><span class="line">			<span class="string">&quot;iss&quot;</span>: <span class="string">&quot;my-auth-server&quot;</span>,</span><br><span class="line">			<span class="string">&quot;sub&quot;</span>: <span class="string">&quot;123&quot;</span>,</span><br><span class="line">			<span class="string">&quot;scp&quot;</span>: <span class="string">&quot;read:hellos write:hellos&quot;</span>,</span><br><span class="line">		&#125;).SignedString(ecPrivateKey)</span><br><span class="line">	<span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">		log.Fatal(err)</span><br><span class="line">	&#125;</span><br><span class="line"></span><br><span class="line">	fmt.Println(s)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div>

<p>実行するとJWTトークンが標準出力されます。これをAuthorizationヘッダーに利用します。</p>
<div class="code-block"><figure class="highlight console"><input type="checkbox" id="code-wrap-162sgu1-3" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-162sgu1-3" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">go run .</span></span><br><span class="line">eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJteS1hdXRoLXNlcnZlciIsInNjcCI6InJlYWQ6aGVsbG9zIHdyaXRlOmhlbGxvcyIsInN1YiI6IjEyMyJ9.AM5-XwIJM0HBxHeaUt2SXU7fU8UXQhet6DfzP7i0JoTVLwbme36NZ-rG_8URqtUkQ2knvi7D3iydvCGgoDGdHm41Ae3aMDNG-yjwUiH7O9xJVLPly2EkwQC0GdsZU6ax-99t0ePDaeJaNf7k799hgxDQ3op9KCNTr8pDfvR2a6PkLvfQ</span><br></pre></td></tr></table></figure></div>

<h2 id="openapi-yaml">openapi.yaml</h2><p><code>openapi.yaml</code> の security schemes は次のように記載しています。先述の通り、OAuth 2.0での認可サーバ、OIDCでのIdPは利用しない構成のため、authorizationUrlなどの値はすべてダミー値であり、検証には使われません。</p>
<div class="code-block"><figure class="highlight yaml"><input type="checkbox" id="code-wrap-162sgu1-4" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-162sgu1-4" title="コードの折り返しを切り替える"></label><figcaption><span>openapi.yaml 抜粋</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="attr">components:</span></span><br><span class="line">  <span class="comment"># ...省略...</span></span><br><span class="line">  <span class="attr">securitySchemes:</span></span><br><span class="line">    <span class="attr">Bearer:</span></span><br><span class="line">      <span class="attr">type:</span> <span class="string">http</span></span><br><span class="line">      <span class="attr">scheme:</span> <span class="string">bearer</span></span><br><span class="line">      <span class="attr">bearerFormat:</span> <span class="string">JWT</span></span><br><span class="line">      <span class="attr">description:</span> <span class="string">&#x27;Bearerトークン認可&#x27;</span></span><br><span class="line">    <span class="attr">OAuth2:</span></span><br><span class="line">      <span class="attr">type:</span> <span class="string">oauth2</span></span><br><span class="line">      <span class="attr">flows:</span></span><br><span class="line">        <span class="attr">authorizationCode:</span></span><br><span class="line">          <span class="attr">authorizationUrl:</span> <span class="string">&#x27;https://example.com/oauth2/authorize&#x27;</span></span><br><span class="line">          <span class="attr">tokenUrl:</span> <span class="string">&#x27;https://example.com/oauth2/token&#x27;</span></span><br><span class="line">          <span class="attr">refreshUrl:</span> <span class="string">&#x27;https://example.com/oauth2/refresh&#x27;</span></span><br><span class="line">          <span class="attr">scopes:</span></span><br><span class="line">            <span class="attr">&#x27;write:hellos&#x27;:</span> <span class="string">modify</span> <span class="string">hello</span> <span class="string">in</span> <span class="string">your</span> <span class="string">account</span></span><br><span class="line">            <span class="attr">&#x27;read:hellos&#x27;:</span> <span class="string">read</span> <span class="string">hello</span> <span class="string">in</span> <span class="string">your</span> <span class="string">account</span></span><br><span class="line">      <span class="attr">description:</span> <span class="string">&#x27;OAuth 2.0認可&#x27;</span></span><br><span class="line">    <span class="attr">OIDC:</span></span><br><span class="line">      <span class="attr">type:</span> <span class="string">openIdConnect</span></span><br><span class="line">      <span class="attr">openIdConnectUrl:</span> <span class="string">https://example.com/.well-known/openid-configuration</span></span><br><span class="line">      <span class="attr">description:</span> <span class="string">&#x27;OpenID Connect&#x27;</span></span><br></pre></td></tr></table></figure></div>

<p>各エンドポイントは次のように定義します。認証なし・Bearer認証・OAuth2.0認証・OIDC認証の4パターンです。</p>
<div class="code-block"><figure class="highlight yaml"><input type="checkbox" id="code-wrap-162sgu1-5" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-162sgu1-5" title="コードの折り返しを切り替える"></label><figcaption><span>openapi.yaml 抜粋</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="attr">paths:</span></span><br><span class="line">  <span class="string">&#x27;/hello&#x27;</span><span class="string">:</span></span><br><span class="line">    <span class="attr">get:</span></span><br><span class="line">      <span class="attr">tags:</span></span><br><span class="line">        <span class="bullet">-</span> <span class="string">ping</span></span><br><span class="line">      <span class="attr">summary:</span> <span class="string">hello👋</span></span><br><span class="line">      <span class="attr">operationId:</span> <span class="string">hello</span></span><br><span class="line">      <span class="attr">responses:</span></span><br><span class="line">        <span class="attr">&#x27;200&#x27;:</span></span><br><span class="line">          <span class="string">$ref:</span> <span class="string">&#x27;#/components/responses/Hello&#x27;</span></span><br><span class="line">  <span class="string">&#x27;/hello-bearer&#x27;</span><span class="string">:</span></span><br><span class="line">    <span class="attr">get:</span></span><br><span class="line">      <span class="attr">tags:</span></span><br><span class="line">        <span class="bullet">-</span> <span class="string">ping</span></span><br><span class="line">      <span class="attr">summary:</span> <span class="string">hello</span> <span class="string">bearer👋</span></span><br><span class="line">      <span class="attr">operationId:</span> <span class="string">helloBearer</span></span><br><span class="line">      <span class="attr">responses:</span></span><br><span class="line">        <span class="attr">&#x27;200&#x27;:</span></span><br><span class="line">          <span class="string">$ref:</span> <span class="string">&#x27;#/components/responses/Hello&#x27;</span></span><br><span class="line">      <span class="attr">security:</span></span><br><span class="line">        <span class="bullet">-</span> <span class="attr">Bearer:</span> []</span><br><span class="line">  <span class="string">&#x27;/hello-oauth2&#x27;</span><span class="string">:</span></span><br><span class="line">    <span class="attr">get:</span></span><br><span class="line">      <span class="attr">tags:</span></span><br><span class="line">        <span class="bullet">-</span> <span class="string">ping</span></span><br><span class="line">      <span class="attr">summary:</span> <span class="string">hello</span> <span class="string">oauth2👋</span></span><br><span class="line">      <span class="attr">operationId:</span> <span class="string">helloOAuth2</span></span><br><span class="line">      <span class="attr">responses:</span></span><br><span class="line">        <span class="attr">&#x27;200&#x27;:</span></span><br><span class="line">          <span class="string">$ref:</span> <span class="string">&#x27;#/components/responses/Hello&#x27;</span></span><br><span class="line">      <span class="attr">security:</span></span><br><span class="line">        <span class="bullet">-</span> <span class="attr">OAuth2:</span></span><br><span class="line">            <span class="bullet">-</span> <span class="string">&#x27;write:hellos&#x27;</span></span><br><span class="line">            <span class="bullet">-</span> <span class="string">&#x27;read:hellos&#x27;</span></span><br><span class="line">  <span class="string">&#x27;/hello-oidc&#x27;</span><span class="string">:</span></span><br><span class="line">    <span class="attr">get:</span></span><br><span class="line">      <span class="attr">tags:</span></span><br><span class="line">        <span class="bullet">-</span> <span class="string">ping</span></span><br><span class="line">      <span class="attr">summary:</span> <span class="string">hello</span> <span class="string">openid</span> <span class="string">connect👋</span></span><br><span class="line">      <span class="attr">operationId:</span> <span class="string">helloOIDC</span></span><br><span class="line">      <span class="attr">responses:</span></span><br><span class="line">        <span class="attr">&#x27;200&#x27;:</span></span><br><span class="line">          <span class="string">$ref:</span> <span class="string">&#x27;#/components/responses/Hello&#x27;</span></span><br><span class="line">      <span class="attr">security:</span></span><br><span class="line">        <span class="bullet">-</span> <span class="attr">OIDC:</span></span><br><span class="line">            <span class="bullet">-</span> <span class="string">&#x27;write:hellos&#x27;</span></span><br><span class="line">            <span class="bullet">-</span> <span class="string">&#x27;read:hellos&#x27;</span></span><br></pre></td></tr></table></figure></div>

<p><code>openapi.yaml</code>の全体は https://github.com/ma91n/summer2024/blob/main/openapi.yaml を参照ください。</p>
<h2 id="検証項目">検証項目</h2><p>この <code>openapi.yaml</code> を元にコードを生成し、次の内容がどのように変化するか確認しました。</p>
<ol>
<li>Bearer、OAuth2、OIDC の認証設定によって生成コードがどのように変わるか、変わらないのか</li>
<li>生成された認証設定のコードを各フレームワークでどのように実装するか</li>
<li>OAuth2、OIDC でスコープ（<code>write:hellos</code> などの部分）がどう生成コードに影響を与えるか</li>
</ol>
<h2 id="結果サマリ">結果サマリ</h2><p>【凡例】 ✅️対応あり ✘対応なし</p>
<div class="scroll"><table>
<thead>
<tr>
<th>Name</th>
<th>Bearer</th>
<th>OAuth2</th>
<th>OIDC</th>
<th>Bearerトークン取得処理</th>
<th>スコープ対応</th>
<th>所感</th>
</tr>
</thead>
<tbody><tr>
<td>ogen</td>
<td>✅️</td>
<td>✅️</td>
<td>✘</td>
<td>コード生成支援あり</td>
<td>コード生成支援あり</td>
<td>後発だけあり最も洗練度が高い</td>
</tr>
<tr>
<td>oapi-codegen</td>
<td>✅️</td>
<td>✅️</td>
<td>✅️</td>
<td>自前実装</td>
<td>コード生成支援あり</td>
<td>現時点でも自由度高く実装可能</td>
</tr>
<tr>
<td>openapi-generator</td>
<td>✘</td>
<td>✘</td>
<td>✘</td>
<td>自前実装</td>
<td>自前実装</td>
<td>認可処理はサポート外なのが惜しい</td>
</tr>
</tbody></table></div>
<p>今回の検証における、おすすめ度は記載順で、<code>ogen</code> &gt;&#x3D; <code>oapi-codegen</code> &gt; <code>openapi-generator</code> といった感覚です。別の角度では全く結果が変わることも想定されますので、あくまで判断材料の一部としての利用、認識いただければです。</p>
<h2 id="1-ogen">1. ogen</h2><p>セットアップです。</p>
<div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-162sgu1-6" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-162sgu1-6" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="comment"># インストール</span></span><br><span class="line">go install -v github.com/ogen-go/ogen/cmd/ogen@v1.3.0</span><br><span class="line"></span><br><span class="line"><span class="comment"># コード生成</span></span><br><span class="line">ogen --target api --clean openapi.yaml</span><br></pre></td></tr></table></figure></div>

<p>そのまま実行すると以下のエラーが表示されます。<code>ogen</code> はサーバサイドコード生成だけど <code>OIDC</code> が未対応でした。</p>
<div class="code-block"><figure class="highlight console"><input type="checkbox" id="code-wrap-162sgu1-7" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-162sgu1-7" title="コードの折り返しを切り替える"></label><figcaption><span>openIdConnect未対応のメッセージ</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">ogen --target api --clean openapi.yaml</span></span><br><span class="line">Feature &quot;openIdConnect security&quot; is not implemented yet.</span><br></pre></td></tr></table></figure></div>

<p>回避としては同一改装に <code>ogen.yaml</code> を用意して次のスキップ設定を追加します（コマンドラインオプションに設定ファイルの指定は不要で、自動で読み込まれるようです）。</p>
<div class="code-block"><figure class="highlight yaml"><input type="checkbox" id="code-wrap-162sgu1-8" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-162sgu1-8" title="コードの折り返しを切り替える"></label><figcaption><span>ogen.yaml</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="attr">generator:</span></span><br><span class="line">  <span class="attr">ignore_not_implemented:</span> [<span class="string">&quot;openIdConnect security&quot;</span>]</span><br></pre></td></tr></table></figure></div>

<p><code>ogen.yaml</code> で何が指定できるかはドキュメントに記載が見当たらずでした。ご存知の方はX（旧Twitter）でコメントいただけると幸いです。</p>
<p>さて、<code>ogen</code>ですが各エンドポイントの実装詳細は ogensample&#x2F;hello_handler.go を参照いただきたいですが、感覚で言うと生成コードはシンプルでわかりやすく、ハマるポイントは少ないです。</p>
<p>ただ認可側の実装はドキュメントのMisc &gt; Request lifecycle &gt; security-handlersに記載が少しある程度で少し推測が必要でした。</p>
<p>生成コードを読んでいった方が早いかもしれません。ドキュメントと生成コードを見比べていくと、<code>api/SecurityHandler</code> インターフェースを満たす必要があるとわかります。Bearer認証とOAuth2.0認証ごとに呼ばれる関数が異なる作りのようです。さらに同じ認証方式だけど、あるエンドポイントだけで挙動を変えたい場合は、<code>operationName</code> （<code>openapi.yaml</code> で定義した <code>operationId</code> が入っていました）を用いて拡張可能な作りです。親切！</p>
<div class="code-block"><figure class="highlight go"><input type="checkbox" id="code-wrap-162sgu1-9" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-162sgu1-9" title="コードの折り返しを切り替える"></label><figcaption><span>満たすべきインターフェース security_handler.go</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">type</span> SecurityHandler <span class="keyword">interface</span> &#123; bn</span><br><span class="line">	<span class="comment">// Bearerトークン認可. operationNameにはhelloやhelloBearerなどoperationIdの値が入る</span></span><br><span class="line">	HandleBearer(ctx context.Context, operationName <span class="type">string</span>, t Bearer) (context.Context, <span class="type">error</span>)</span><br><span class="line"></span><br><span class="line">	<span class="comment">// OAuth 2.0認可.</span></span><br><span class="line">	HandleOAuth2(ctx context.Context, operationName <span class="type">string</span>, t OAuth2) (context.Context, <span class="type">error</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div>

<p>ogenが生成した <code>Bearer</code> や <code>OAuth2</code> の構造体もシンプル。Tokenはリクエストヘッダの <code>Authorization: Bearer &lt;token&gt;</code> の <code>&lt;token&gt;</code> の値が入っています。<code>OAuth2</code> 側の<code>Scopes</code> は<code>openapi.yaml</code> 側で指定した値が入っています。</p>
<figure class="highlight go"><figcaption><span>oas_schemas_gen.go</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">type</span> Bearer <span class="keyword">struct</span> &#123;</span><br><span class="line">	Token <span class="type">string</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> OAuth2 <span class="keyword">struct</span> &#123;</span><br><span class="line">	Token  <span class="type">string</span></span><br><span class="line">	Scopes []<span class="type">string</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<p>これを満たすように <code>MySecurityHandler</code> を実装します。トークンのチェックはかなり端折った実装になっています。本来は発行者（<code>iss</code>）、アルゴリズム（<code>alg</code>）、用途（<code>aud</code>）、期限（<code>exp</code>）なども確認する必要があるでしょう。</p>
<div class="code-block"><figure class="highlight go"><input type="checkbox" id="code-wrap-162sgu1-10" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-162sgu1-10" title="コードの折り返しを切り替える"></label><figcaption><span>security_handler.go</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">type</span> UserClaim <span class="keyword">struct</span> &#123;</span><br><span class="line">	jwt.RegisteredClaims</span><br><span class="line">	Scope <span class="type">string</span> <span class="string">`json:&quot;scp&quot;`</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> MySecurityHandler <span class="keyword">struct</span>&#123;&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(o MySecurityHandler)</span></span> HandleBearer(ctx context.Context, operationName <span class="type">string</span>, t api.Bearer) (context.Context, <span class="type">error</span>) &#123;</span><br><span class="line">	<span class="keyword">return</span> o.handleToken(ctx, t.Token, []<span class="type">string</span>&#123;&#125;)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(o MySecurityHandler)</span></span> HandleOAuth2(ctx context.Context, operationName <span class="type">string</span>, t api.OAuth2) (context.Context, <span class="type">error</span>) &#123;</span><br><span class="line">	<span class="keyword">return</span> o.handleToken(ctx, t.Token, t.Scopes)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(o MySecurityHandler)</span></span> handleToken(ctx context.Context, jwtToken <span class="type">string</span>, expectedClaims []<span class="type">string</span>) (context.Context, <span class="type">error</span>) &#123;</span><br><span class="line">	<span class="keyword">var</span> userClaim UserClaim</span><br><span class="line">	token, err := jwt.ParseWithClaims(jwtToken, &amp;userClaim, <span class="function"><span class="keyword">func</span><span class="params">(token *jwt.Token)</span></span> (any, <span class="type">error</span>) &#123;</span><br><span class="line">		blockPub, _ := pem.Decode([]<span class="type">byte</span>(jwtKey))</span><br><span class="line">		<span class="keyword">return</span> x509.ParsePKIXPublicKey(blockPub.Bytes)</span><br><span class="line">	&#125;)</span><br><span class="line">	<span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">		<span class="keyword">return</span> ctx, err</span><br><span class="line">	&#125;</span><br><span class="line"></span><br><span class="line">	<span class="keyword">if</span> !token.Valid &#123;</span><br><span class="line">		<span class="keyword">return</span> ctx, errors.New(<span class="string">&quot;invalid token&quot;</span>)</span><br><span class="line">	&#125;</span><br><span class="line"></span><br><span class="line">	<span class="keyword">if</span> err = checkTokenClaims(expectedClaims, userClaim); err != <span class="literal">nil</span> &#123;</span><br><span class="line">		<span class="keyword">return</span> ctx, fmt.Errorf(<span class="string">&quot;token claims don&#x27;t match: %w&quot;</span>, err)</span><br><span class="line">	&#125;</span><br><span class="line"></span><br><span class="line">  	<span class="comment">// TODO その他、必要なチェックを実施</span></span><br><span class="line"></span><br><span class="line">	<span class="keyword">return</span> <span class="literal">nil</span>, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// checkTokenClaims はスコープのチェックを行う</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">checkTokenClaims</span><span class="params">(expectedClaims []<span class="type">string</span>, t UserClaim)</span></span> <span class="type">error</span> &#123;</span><br><span class="line">	<span class="comment">// ...省略...</span></span><br><span class="line">	<span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div>

<p>上記の2つのハンドラーをmain.goで呼び出して完成です。</p>
<div class="code-block"><figure class="highlight go"><input type="checkbox" id="code-wrap-162sgu1-11" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-162sgu1-11" title="コードの折り返しを切り替える"></label><figcaption><span>main.go</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">	<span class="string">&quot;log&quot;</span></span><br><span class="line">	<span class="string">&quot;net/http&quot;</span></span><br><span class="line"></span><br><span class="line">	<span class="string">&quot;githu.com/ma91n/summer2024/ogensample/api&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">	srv, err := api.NewServer(&amp;HelloHandler&#123;&#125;, MySecurityHandler&#123;&#125;)</span><br><span class="line">	<span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">		log.Fatal(err)</span><br><span class="line">	&#125;</span><br><span class="line"></span><br><span class="line">	<span class="keyword">if</span> err := http.ListenAndServe(<span class="string">&quot;:8080&quot;</span>, srv); err != <span class="literal">nil</span> &#123;</span><br><span class="line">		log.Fatal(err)</span><br><span class="line">	&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div>

<p><code>go run .</code> で実行すると、<code>8080</code> ポートで起動します。</p>
<p><code>curl</code> で実行して動作確認します。</p>
<div class="code-block"><figure class="highlight console"><input type="checkbox" id="code-wrap-162sgu1-12" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-162sgu1-12" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">curl localhost:8080/hello</span></span><br><span class="line">&#123;&quot;message&quot;:&quot;hello&quot;&#125;</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">make curl-bearer</span></span><br><span class="line">curl -H &quot;Authorization: Bearer &lt;トークン&gt;&quot; localhost:8080/hello-bearer</span><br><span class="line">&#123;&quot;message&quot;:&quot;hello&quot;&#125;</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">curl -H <span class="string">&quot;Authorization: Bearer &lt;トークン&gt;&quot;</span> localhost:8080/hello-oauth2</span></span><br><span class="line">&#123;&quot;message&quot;:&quot;hello&quot;&#125;</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">curl -H <span class="string">&quot;Authorization: Bearer &lt;トークン&gt;&quot;</span> localhost:8080/hello-oidc</span></span><br><span class="line">&#123;&quot;message&quot;:&quot;hello&quot;&#125;</span><br></pre></td></tr></table></figure></div>

<p>あえて失敗させるケースでの挙動を見てみます。</p>
<div class="code-block"><figure class="highlight console"><input type="checkbox" id="code-wrap-162sgu1-13" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-162sgu1-13" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">Authorizationヘッダなしで動かすケース</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">curl localhost:8080/hello-bearer</span></span><br><span class="line">&#123;&quot;error_message&quot;:&quot;operation HelloBearer: security \&quot;\&quot;: security requirement is not satisfied&quot;&#125;</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">JWTのシグネチャを1文字書き換えたケース</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">curl -H <span class="string">&quot;Authorization: Bearer &lt;1文字シグネチャを書き換えた不正なトークン&gt;&quot;</span> localhost:8080/hello-bearer</span></span><br><span class="line">&#123;&quot;error_message&quot;:&quot;operation HelloBearer: security \&quot;Bearer\&quot;: token signature is invalid: crypto/ecdsa: verification error&quot;&#125;</span><br></pre></td></tr></table></figure></div>

<p>詳細は割愛しますが、JWTトークンにユーザーIDなどが含まれており、各Handlerアプリ側で利用したい場合も、<code>ctx</code> に詰めて連携可能であり、使いやすインターフェース（フレームワーク）だと感じました。</p>
<p>ogenのサーバサイドコード生成について、まとめると次のような所感です。</p>
<ul>
<li>OIDCには対応していない</li>
<li>Bearer、OAuth2といった種別ごとに一律、ミドルウェアのような形式で認可ロジックを実装する</li>
<li>各エンドポイント毎に挙動を変えたい場合は、引数のoperationIdの値を利用して切り替え可能</li>
<li>Bearer、OAuth2で生成コードの差分はほぼ無いが、スコープフィールドだけが増える</li>
<li>JWTトークンのパースや検証を自前で実装する</li>
<li>JWTトークンの値を後続に引き渡したい場合は、 <code>context.Context</code> を経由する</li>
</ul>
<h2 id="2-oapi-codegen">2. oapi-codegen</h2><p><code>ogen</code>より先発だけあって、利用実績も多数な<code>oapi-codegen</code>ですが、2024年5月にOrganizationが<code>deepmap</code>から<code>oapi-codegen</code> に変わったようです。</p>
<p>セットアップです。</p>
<div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-162sgu1-14" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-162sgu1-14" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="comment"># インストール</span></span><br><span class="line">go install github.com/deepmap/oapi-codegen/v2/cmd/oapi-codegen@v2.2.0</span><br><span class="line"></span><br><span class="line"><span class="comment"># コード生成</span></span><br><span class="line">oapi-codegen --config=config.yaml openapi.yaml</span><br></pre></td></tr></table></figure></div>

<p><code>oapi-codegen</code> は<code>Echo</code>、<code>Gin</code>、<code>net/http</code> などに沿ったコードを生成できますが、今回は <code>net/http</code> 向けで出力します。また、リクエスト／レスポンスを構造体にマッピングまで行ってくれる、<code>strict-server</code> モードを有効にします。これを踏まえ、先程のコード生成コマンドの引数で渡していた<code>config.yaml</code> を以下のように設定しました。</p>
<figure class="highlight yaml"><figcaption><span>config.yaml</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="attr">package:</span> <span class="string">api</span></span><br><span class="line"><span class="attr">generate:</span></span><br><span class="line">  <span class="attr">std-http-server:</span> <span class="literal">true</span></span><br><span class="line">  <span class="attr">models:</span> <span class="literal">true</span></span><br><span class="line">  <span class="attr">strict-server:</span> <span class="literal">true</span></span><br><span class="line">  <span class="attr">embedded-spec:</span> <span class="literal">true</span></span><br><span class="line"><span class="attr">output:</span> <span class="string">api/gen.go</span></span><br></pre></td></tr></table></figure>

<p>各エンドポイントは <code>oapi-codegen</code> が生成した <code>api/gen.go</code> の <code>StrictServerInterface</code>インターフェースを実装する必要があります。全体像は hello_handler.go を参照いただきたいですが、<code>ref</code> でオブエジェクト参照すると、<code>ogen</code>と比べ生成されたコードにネストが発生するところが、少しもどかしさを感じます。</p>
<div class="code-block"><figure class="highlight go"><input type="checkbox" id="code-wrap-162sgu1-15" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-162sgu1-15" title="コードの折り返しを切り替える"></label><figcaption><span>hello_handler.go（抜粋）</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">type</span> HelloServer <span class="keyword">struct</span>&#123;&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s HelloServer)</span></span> Hello(_ context.Context, _ api.HelloRequestObject) (api.HelloResponseObject, <span class="type">error</span>) &#123;</span><br><span class="line">	<span class="keyword">return</span> api.Hello200JSONResponse&#123;HelloJSONResponse: api.HelloJSONResponse&#123;Message: ptr(<span class="string">&quot;hello&quot;</span>)&#125;&#125;, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s HelloServer)</span></span> HelloBearer(_ context.Context, _ api.HelloBearerRequestObject) (api.HelloBearerResponseObject, <span class="type">error</span>) &#123;</span><br><span class="line">	<span class="keyword">return</span> api.HelloBearer200JSONResponse&#123;HelloJSONResponse: api.HelloJSONResponse&#123;Message: ptr(<span class="string">&quot;hello&quot;</span>)&#125;&#125;, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s HelloServer)</span></span> HelloOAuth2(_ context.Context, _ api.HelloOAuth2RequestObject) (api.HelloOAuth2ResponseObject, <span class="type">error</span>) &#123;</span><br><span class="line">	<span class="keyword">return</span> api.HelloOAuth2200JSONResponse&#123;HelloJSONResponse: api.HelloJSONResponse&#123;Message: ptr(<span class="string">&quot;hello&quot;</span>)&#125;&#125;, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div>

<p>認証部分は、ミドルウェアとして実装します。リポジトリ側にexamples実装があるため、それを参考にするとよいでしょう。RAEDMEの記載を見る限り、現状はミドルウェアで実装する必要があるが、将来的にはファーストクラスサポートしていくよとあり、期待です。</p>
<p>サンプル実装を元に <code>main.go</code> を実装します。ミドルウェアですが、<code>nethttpmiddleware</code>という<code>oapi-codegen</code>が用意してくれたパッケージを利用します。</p>
<div class="code-block"><figure class="highlight go"><input type="checkbox" id="code-wrap-162sgu1-16" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-162sgu1-16" title="コードの折り返しを切り替える"></label><figcaption><span>main.go</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="comment">// ...中略...</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">	spec, err := api.GetSwagger()</span><br><span class="line">	<span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">		log.Fatalln(<span class="string">&quot;loading spec: &quot;</span>, err)</span><br><span class="line">	&#125;</span><br><span class="line">	spec.Servers = <span class="literal">nil</span></span><br><span class="line"></span><br><span class="line">	<span class="comment">// 認証処理はミドルウェアとして実装</span></span><br><span class="line">	mw := nethttpmiddleware.OapiRequestValidatorWithOptions(spec,</span><br><span class="line">		&amp;nethttpmiddleware.Options&#123;</span><br><span class="line">			Options: openapi3filter.Options&#123;</span><br><span class="line">				AuthenticationFunc: NewAuthenticator(), <span class="comment">// ★NewAuthenticatorは個別実装の部分</span></span><br><span class="line">			&#125;,</span><br><span class="line">		&#125;)</span><br><span class="line"></span><br><span class="line">	strictHandler := api.NewStrictHandler(HelloServer&#123;&#125;, <span class="literal">nil</span>)</span><br><span class="line">	h := api.HandlerFromMux(strictHandler, http.NewServeMux())</span><br><span class="line"></span><br><span class="line">	s := &amp;http.Server&#123;</span><br><span class="line">		Handler: mw(h),</span><br><span class="line">		Addr:    <span class="string">&quot;0.0.0.0:8080&quot;</span>,</span><br><span class="line">	&#125;</span><br><span class="line">	log.Fatal(s.ListenAndServe())</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div>

<p>ミドルウェアの実体としては次のようなインターフェースを実装します。現時点ではセキュリティスキーマごちゃまぜですので、入力値でスイッチして切り替えます。<code>openapi3filter.AuthenticationInput</code> は<code>openapi.yaml</code> の解析結果や <code>http.Request</code> も取れるため、やろうと思えばすべて判定できます。</p>
<div class="code-block"><figure class="highlight go"><input type="checkbox" id="code-wrap-162sgu1-17" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-162sgu1-17" title="コードの折り返しを切り替える"></label><figcaption><span>jwt_authenticator.go</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">NewAuthenticator</span><span class="params">()</span></span> openapi3filter.AuthenticationFunc &#123;</span><br><span class="line">	<span class="keyword">return</span> <span class="function"><span class="keyword">func</span><span class="params">(ctx context.Context, input *openapi3filter.AuthenticationInput)</span></span> <span class="type">error</span> &#123;</span><br><span class="line">		securitySchemeName := input.SecuritySchemeName</span><br><span class="line">		<span class="keyword">switch</span> securitySchemeName &#123;</span><br><span class="line">		<span class="keyword">case</span> <span class="string">&quot;&quot;</span>: <span class="comment">// 認証なし</span></span><br><span class="line">			<span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">		<span class="keyword">case</span> <span class="string">&quot;Bearer&quot;</span>:</span><br><span class="line">			<span class="keyword">return</span> validateSecurityScheme(input)</span><br><span class="line">		<span class="keyword">case</span> <span class="string">&quot;OAuth2&quot;</span>:</span><br><span class="line">			<span class="keyword">return</span> validateSecurityScheme(input)</span><br><span class="line">		<span class="keyword">case</span> <span class="string">&quot;OIDC&quot;</span>:</span><br><span class="line">			<span class="keyword">return</span> validateSecurityScheme(input)</span><br><span class="line">		<span class="keyword">default</span>:</span><br><span class="line">			<span class="built_in">panic</span>(<span class="string">&quot;not supported security scheme &quot;</span> + securitySchemeName)</span><br><span class="line">		&#125;</span><br><span class="line">	&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">validateSecurityScheme</span><span class="params">(input *openapi3filter.AuthenticationInput)</span></span> <span class="type">error</span> &#123;</span><br><span class="line">	jws, err := getJWSFromRequest(input.RequestValidationInput.Request)</span><br><span class="line">	<span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">		<span class="keyword">return</span> fmt.Errorf(<span class="string">&quot;getting jws: %w&quot;</span>, err)</span><br><span class="line">	&#125;</span><br><span class="line"></span><br><span class="line">	<span class="keyword">var</span> userClaim UserClaim</span><br><span class="line">	token, err := jwt.ParseWithClaims(jws, &amp;userClaim, <span class="function"><span class="keyword">func</span><span class="params">(token *jwt.Token)</span></span> (any, <span class="type">error</span>) &#123;</span><br><span class="line">		<span class="comment">// 参考: https://stackoverflow.com/questions/21322182/how-to-store-ecdsa-private-key-in-go</span></span><br><span class="line">		blockPub, _ := pem.Decode([]<span class="type">byte</span>(jwtKey))</span><br><span class="line">		<span class="keyword">return</span> x509.ParsePKIXPublicKey(blockPub.Bytes)</span><br><span class="line">	&#125;)</span><br><span class="line">	<span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">		<span class="keyword">return</span> fmt.Errorf(<span class="string">&quot;validating JWS: %w&quot;</span>, err)</span><br><span class="line">	&#125;</span><br><span class="line"></span><br><span class="line">	<span class="comment">// ...ここからはogen版と同様であるため省略...</span></span><br><span class="line"></span><br><span class="line">	<span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div>

<p>サーバを <code>go run .</code> で起動して、動作確認します。</p>
<div class="code-block"><figure class="highlight console"><input type="checkbox" id="code-wrap-162sgu1-18" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-162sgu1-18" title="コードの折り返しを切り替える"></label><figcaption><span>成功ケース</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">curl localhost:8080/hello</span></span><br><span class="line">&#123;&quot;message&quot;:&quot;hello&quot;&#125;</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">make curl-bearer</span></span><br><span class="line">curl -H &quot;Authorization: Bearer &lt;トークン&gt;&quot; localhost:8080/hello-bearer</span><br><span class="line">&#123;&quot;message&quot;:&quot;hello&quot;&#125;</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">curl -H <span class="string">&quot;Authorization: Bearer &lt;トークン&gt;&quot;</span> localhost:8080/hello-oauth2</span></span><br><span class="line">&#123;&quot;message&quot;:&quot;hello&quot;&#125;</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">curl -H <span class="string">&quot;Authorization: Bearer &lt;トークン&gt;&quot;</span> localhost:8080/hello-oidc</span></span><br><span class="line">&#123;&quot;message&quot;:&quot;hello&quot;&#125;</span><br></pre></td></tr></table></figure></div>

<p>エラーメッセージはハンドリングがデフォルトのままなので、JSONではなく文字列が返ってきていますが、チェック自体はできていることがわかります。</p>
<div class="code-block"><figure class="highlight console"><input type="checkbox" id="code-wrap-162sgu1-19" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-162sgu1-19" title="コードの折り返しを切り替える"></label><figcaption><span>失敗ケース</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">Authorizationヘッダなしで動かすケース</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">curl localhost:8080/hello-bearer</span></span><br><span class="line">security requirements failed: getting jws: authorization header is missing</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">JWTのシグネチャを1文字書き換えたケース</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">curl -H <span class="string">&quot;Authorization: Bearer &lt;1文字シグネチャを書き換えた不正なトークン&gt;&quot;</span> localhost:8080/hello-bearer</span></span><br><span class="line">security requirements failed: validating JWS: token is malformed: could not JSON decode header: invalid character &#x27;\x13&#x27; looking for beginning of value</span><br></pre></td></tr></table></figure></div>

<p>oapi-codegenのサーバサイドコード生成について、まとめると次のような所感です。</p>
<ul>
<li><code>openapi.yaml</code> の定義上でオブジェクトを <code>$ref</code> 参照させると少しコードが冗長に見える（回避策の有無は未調査）</li>
<li>Bearer、OAuth2、OIDCのチェックは現時点ではミドルウェアで実装し、コード生成上のサポートは弱い（将来的に解消する方向とREADMEに記載あり）</li>
<li>ミドルウェアでは、判定に必要な情報はすべて取得できるため、該当のリクエストがBearer、OAuth2、OIDCのどれでチェックすべきか、operationIdを用いた各エンドポイント固有の挙動を追加するなど柔軟に開発できる自由度がある</li>
<li>試していないが、JWTトークンを解析した結果は <code>http.Request</code> を取得できるため <code>Request.WithContext()</code> を用いた <code>context.Context</code> 経由で各エンドポイント側に渡すことが可能だと考えられる</li>
<li>JWTトークンのパースどころか、リクエストヘッダから取得するところまで自前開発が必要（とはいえ、大したコード量にはならない）</li>
<li>様々な出力モードがあるため、検索結果が別の設定モードの場合があり、見極め力が必要な場合がある</li>
</ul>
<h2 id="3-openapi-generator">3. openapi-generator</h2><p>最も有名なコード生成ツールである <code>openapi-generator</code> を試します。様々な言語に対応していますが、記事の趣旨からGo言語かつサーバサイドに絞って生成します。</p>
<p>セットアップです。</p>
<div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-162sgu1-20" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-162sgu1-20" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="comment"># インストール</span></span><br><span class="line">npm install @openapitools/openapi-generator-cli -g</span><br><span class="line"></span><br><span class="line"><span class="comment"># コード生成</span></span><br><span class="line">openapi-generator-cli version-manager <span class="built_in">set</span> 7.8.0</span><br></pre></td></tr></table></figure></div>

<p>各エンドポイントのコードは生成された<code>openapi/api_ping_service.go</code> のTODOを埋めて実装します。DO NOT EDITを手動で消しつつ、 <code>.openapi-generator-ignore</code> に上記のパスを追加することで、上書きされることを防ぐ必要があります。</p>
<div class="code-block"><figure class="highlight go"><input type="checkbox" id="code-wrap-162sgu1-21" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-162sgu1-21" title="コードの折り返しを切り替える"></label><figcaption><span>openapi/api_ping_service.go（抜粋）</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">type</span> PingAPIService <span class="keyword">struct</span> &#123;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">NewPingAPIService</span><span class="params">()</span></span> *PingAPIService &#123;</span><br><span class="line">	<span class="keyword">return</span> &amp;PingAPIService&#123;&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Hello - hello👋</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s *PingAPIService)</span></span> Hello(_ context.Context) (ImplResponse, <span class="type">error</span>) &#123;</span><br><span class="line">	<span class="keyword">return</span> Response(http.StatusOK, Hello&#123;Message: <span class="string">&quot;Hello&quot;</span>&#125;), <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// HelloBearer - hello bearer👋</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s *PingAPIService)</span></span> HelloBearer(ctx context.Context) (ImplResponse, <span class="type">error</span>) &#123;</span><br><span class="line">	<span class="keyword">return</span> s.Hello(ctx)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// HelloOAuth2 - hello oauth2👋</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s *PingAPIService)</span></span> HelloOAuth2(ctx context.Context) (ImplResponse, <span class="type">error</span>) &#123;</span><br><span class="line">	<span class="keyword">return</span> s.Hello(ctx)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// HelloOIDC - hello openid connect👋</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s *PingAPIService)</span></span> HelloOIDC(ctx context.Context) (ImplResponse, <span class="type">error</span>) &#123;</span><br><span class="line">	<span class="keyword">return</span> s.Hello(ctx)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div>

<p><code>openapi-generator</code> のGoサーバサイド生成ですが、<strong>セキュリティスキーマには未対応</strong> です。generators&#x2F;go-server.md にもその旨が書かれています。コード生成上の支援は現時点では受けられません。</p>
<p>そのため、認証処理はミドルウェアで個別実装する必要があります。デフォルトでは GitHub.com&#x2F;gorilla&#x2F;mux が利用される（chiに切り替えも可能）ので、muxのミドルウェアを実装します。</p>
<p>ミドルウェアとして、そのリクエストが認証なし・Bearer・OAuth2.0・OIDCかどうかは自分で判定する必要があります。また、該当のリクエストがどのスコープを要求するかも自分でマッピングを準備する必要があります。</p>
<div class="code-block"><figure class="highlight go"><input type="checkbox" id="code-wrap-162sgu1-22" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-162sgu1-22" title="コードの折り返しを切り替える"></label><figcaption><span>auth_middleware.go</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Authentication</span><span class="params">(next http.Handler)</span></span> http.Handler &#123;</span><br><span class="line">	<span class="keyword">return</span> http.HandlerFunc(<span class="function"><span class="keyword">func</span><span class="params">(w http.ResponseWriter, r *http.Request)</span></span> &#123;</span><br><span class="line">		<span class="keyword">switch</span> r.URL.Path &#123;</span><br><span class="line">		<span class="keyword">case</span> <span class="string">&quot;/hello&quot;</span>:</span><br><span class="line">			next.ServeHTTP(w, r)</span><br><span class="line">		<span class="keyword">default</span>: <span class="comment">// URL＋HTTPメソッドごとに手動でマッピングするなどの必要がある</span></span><br><span class="line">			<span class="keyword">if</span> err := validateToken(r); err != <span class="literal">nil</span> &#123;</span><br><span class="line">				status := http.StatusUnauthorized</span><br><span class="line">				_ = openapi.EncodeJSONResponse(<span class="keyword">map</span>[<span class="type">string</span>]any&#123;<span class="string">&quot;message&quot;</span>: err.Error()&#125;, &amp;status, w)</span><br><span class="line">				<span class="keyword">return</span></span><br><span class="line">			&#125;</span><br><span class="line">			next.ServeHTTP(w, r)</span><br><span class="line">		&#125;</span><br><span class="line">	&#125;)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">validateToken</span><span class="params">(r *http.Request)</span></span> <span class="type">error</span> &#123;</span><br><span class="line">	jws, err := getJWSFromRequest(r)</span><br><span class="line">	<span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">		<span class="keyword">return</span> fmt.Errorf(<span class="string">&quot;getting jws: %w&quot;</span>, err)</span><br><span class="line">	&#125;</span><br><span class="line"></span><br><span class="line">	<span class="comment">// ...ここからはoapi-codegenと同様なので省略...</span></span><br><span class="line"> 	<span class="comment">// 認可スコープの引き渡しも自前でなんとかする必要がある</span></span><br><span class="line"></span><br><span class="line">	<span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div>

<p>マッピング情報を <code>oepnapi.yaml</code> とダブルメンテすることは実運用上、耐えられないチームが多いと思いますのでそのままでは利用できないという判断を下すことが多いかなと思います。そのため、Yusuke ItoさんのZennブック【Go言語】OpenAPI Generatorを使いこなすスキーマ駆動開発 にある通り、テンプレートがmustacheで書かれており、これを拡張して利用するといった取り組みが必要となるかと思います。</p>
<p><code>openapi-generator</code> のサーバサイドコード生成について、まとめると次のような所感です。</p>
<ul>
<li>記事には書いていなかったが、<code>.openapi-generator</code>、<code>.openapi-generator-ignore</code>、<code>openapitools.json</code>、<code>README.md</code> などGoのコード以外にもファイルが生成して初見は少し驚く</li>
<li>各エンドポイントの実装は迷うことが少ないが、ImplResponseのボディは <code>interface&#123;&#125;</code> であるため、型がふわっとしてしまって残念に感じた（回避方法の有無は未調査）</li>
<li>認可周りのコード生成上の支援が無く、使いこなすにはテンプレートをカスタマイズする勢いが必要そう</li>
<li>Yusuke ItoさんのZennブック[【Go言語】OpenAPI Generatorを使いこなすスキーマ駆動開発]によれば、カスタマイズしたテンプレート実行には、Java環境（Mavenなど）が必要で、メンバーのスキルセット次第では障壁がある（FAT JAR提供とかあったらすいません）</li>
<li>調査すると、他の言語（Swiftなど）の結果が出てくるので、検索ワード力が必要かもしれない</li>
</ul>
<h2 id="さいごに">さいごに</h2><p>OpenAPI 3系かつGoのサーバサイドコード生成に対応した <code>ogen</code>・<code>oapi-codegen</code>・<code>openapi-generator</code>について、Bearer・OAuth2.0・OIDC認可でどのようにコード生成が対応しているか試しました。現時点では <code>ogen</code> が後発だけあって一番垢抜けていて、<code>oapi-codegen</code>も十分に扱いやすい。<code>openapi-generator</code>は玄人向けだなと感じました。</p>
<p>コード生成上はサーバサイドに限ると、Bearer・OAuth2.0・OIDCで変化はほぼ無いため（大半はミドルウェア的に実装するだけ）であるため、サーバサイドの実装視点では <code>openapi.yaml</code> で細かく定義する意味はあまり無い（もちろんWeb APIの利用者視点では有意義な手がかりになるでしょうが）という結果でした。</p>
<p>近い将来では、これらコードジェネレータの領域は、ChatGPTとかclaude.aiに <code>openapi.yaml</code> を入力させてプロンプトエンジニアリングでコード生成させる方向でも広がっていくと良いかなと思っています。コード生成コマンド時のオプションや、設定ファイルなどで行う微調整的な機能は、生成AI側で行ってくれる未来…、来ると良いなぁ。</p>
]]></content>
    <summary type="html">夏といえばコード生成というわけで、HTTP API仕様を定義するOpenAPIの security schemes（認証認可を定義するための箇所）で、Bearer／OAuth2／OpenID Connect 認証を設定すると、各コードジェネレータはどういったコード生成をしてくれるかを調べました。</summary>
    <category term="認証認可" scheme="https://future-architect.github.io/categories/%E8%AA%8D%E8%A8%BC%E8%AA%8D%E5%8F%AF/"/>
    <category term="Go" scheme="https://future-architect.github.io/tags/Go/"/>
    <category term="JWT" scheme="https://future-architect.github.io/tags/JWT/"/>
    <category term="OAuth" scheme="https://future-architect.github.io/tags/OAuth/"/>
    <category term="OIDC" scheme="https://future-architect.github.io/tags/OIDC/"/>
    <category term="OpenAPI" scheme="https://future-architect.github.io/tags/OpenAPI/"/>
    <category term="OpenAPIGenerator" scheme="https://future-architect.github.io/tags/OpenAPIGenerator/"/>
    <category term="OpenSSL" scheme="https://future-architect.github.io/tags/OpenSSL/"/>
    <category term="コード生成" scheme="https://future-architect.github.io/tags/%E3%82%B3%E3%83%BC%E3%83%89%E7%94%9F%E6%88%90/"/>
  </entry>
  <entry>
    <title>Microsoft 365 Developer ProgramでEntraID(旧名AzureAD)にアクセスする</title>
    <link href="https://future-architect.github.io/articles/20240401a/"/>
    <id>https://future-architect.github.io/articles/20240401a/</id>
    <published>2024-03-31T15:00:00.000Z</published>
    <updated>2024-03-31T15:00:00.000Z</updated>
    <author><name>澁川喜規</name></author>
    <content type="html"><![CDATA[<p>過去にいくつかEntraID(旧名AzureAD)の記事を何本も書いていますが、久々にMicrosoft 365 Developer Programにアクセスしたら、どこにEntraIDがあるのか場所が分からなかったのでメモです。Microsoft 365全般で同じかもしれませんが、僕自身はDeveloper Programしか触っていないのでわかりません。</p>
<p>Microsoft 365 Developer Programのウェブサイトで上のメニューのDeveloper Programの「My Dashboard」を選びます。この遷移がわからなくていつもJoin Nowをしていました。</p>
<img fetchpriority="high" src="/images/2024/20240401a/image.png" alt="image.png" width="610" height="287">

<p>こちらがダッシュボードです。</p>
<img src="/images/2024/20240401a/image_2.png" alt="image.png" width="911" height="633" loading="lazy">

<p>ここでGo to subscritpionを選択すると、次のオフィスのポータルっぽいページに移動します。</p>
<img src="/images/2024/20240401a/image_3.png" alt="image.png" width="1200" height="646" loading="lazy">

<p>左のツールバーのAdminアイコンをクリックして・・・上のツールバーにentraidを入れて検索して出てくるIdentityがEntraIDです。</p>
<img src="/images/2024/20240401a/image_4.png" alt="image.png" width="859" height="517" loading="lazy">
]]></content>
    <summary type="html">過去にいくつかEntraIDが、久々にMicrosoft 365 Developer Programにアクセスしたら、どこにEntraIDがあるのか場所が分からなかったのでメモです。</summary>
    <category term="認証認可" scheme="https://future-architect.github.io/categories/%E8%AA%8D%E8%A8%BC%E8%AA%8D%E5%8F%AF/"/>
    <category term="Azure" scheme="https://future-architect.github.io/tags/Azure/"/>
    <category term="EntraID" scheme="https://future-architect.github.io/tags/EntraID/"/>
    <category term="Microsoft" scheme="https://future-architect.github.io/tags/Microsoft/"/>
  </entry>
  <entry>
    <title>Entra IDを使うウェブサービスのバックエンドのテスト</title>
    <link href="https://future-architect.github.io/articles/20231227a/"/>
    <id>https://future-architect.github.io/articles/20231227a/</id>
    <published>2023-12-26T15:00:00.000Z</published>
    <updated>2023-12-26T15:00:00.000Z</updated>
    <author><name>澁川喜規</name></author>
    <content type="html"><![CDATA[<p>Entra ID（旧名Azure AD）は企業での利用の割合が高く、社内システムを作る場合はこれを使って認証して欲しいという要件が積まれることがほとんどでしょう。とはいえ、現物を使ってテストを作るのは都合がよくないこともあったりします。例えば処理時間が延びる（大量のE2Eテストを走らせる場合）とか、たくさんのユーザーのバリエーションを作る場合にそれだけユーザーを登録しないといけないとか、多要素認証の認証をどうするかとか。そんな感じのウェブサービスの単体テストを簡略化する方法について検討して組み込んだので紹介します。</p>
<h2 id="前提">前提</h2><p>Entra IDを組み込む方法については以前のエントリーで紹介しています。</p>
<ul>
<li>MSAL.jsを使ってウェブフロントエンドだけでAzureAD認証する</li>
<li>AzureAD＋MSAL for Goでバッチコマンドの認証</li>
</ul>
<p>後者は他の認証基盤でもだいたい同じですが、前者は、MSAL.jsのおかげでフロントエンドのみで認証が完了するということを紹介しました。多くの場合はサーバーにもコールバックがきたりとか、サーバーもログイン処理の一部に入っているのですが、そこが不要となっています。そのため、バックエンドとしては有効なJWTがリクエストについてきて、その署名の検証だけすればOKという状態となっています。</p>
<p>問題はその署名入りのJWTトークンをどうつくるか、という一点に絞られます。</p>
<p>ちなみに、JWTのTがトークンでJWTトークンと書くと「JSONウェブトークン・トークン」となってしまうのですが、毎回JWTをロングで書くのも面倒ですし、JWTだけだと不親切かと思いますJWTトークンにしています。江戸川や利根川を英語に翻訳するときに「エドガワーリバー」とか「トネガワリバー」とかになるのと同じようなものだと思っていただければ。</p>
<h2 id="自前で署名とJWTトークンを作る">自前で署名とJWTトークンを作る</h2><p>Entra IDの場合、 <code>https://login.microsoftonline.com/&#123;テナントID&#125;/v2.0/.well-known/openid-configuration</code> にアクセスし、その中の <code>jwks_uri</code>というキーを見ると、署名検証のためのJWKの公開鍵が手に入ります。おそらくは<code>https://login.microsoftonline.com/&#123;テナントID&#125;/discovery/v2.0/keys</code>というURLのはずですが。ですが、この公開鍵はEntra IDが内部で保持している秘密鍵を使った署名の検証にしか使えないため、テスト用のJWTトークンを作る場合はこの鍵は使えません。そのため、まずはローカルの公開鍵と暗号鍵のペアを作ります。以下のサイトを使うのが簡単でしょう。</p>
<ul>
<li>https://mkjwk.org/</li>
</ul>
<p>RSAタブを開き以下のように入れるとEntra IDっぽくなります。KeyIDは任意です。</p>
<ul>
<li>Key Size(鍵のビット数): 2048</li>
<li>Key Use(鍵の用途): Signature</li>
<li>Algorithm(アルゴリズム): RS256</li>
<li>Key ID(鍵の識別子): “for test”</li>
<li>Show X.509(): No</li>
</ul>
<p>生成されたら、一番左と一番右をそれぞれJWTトークン作成用に保存しておきます。真ん中はサーバーで利用するのに便利な情報なのでこれも保存しておきます。</p>
<img fetchpriority="high" src="/images/2023/20231227a/image.png" alt="image.png" width="1200" height="986">

<h2 id="JWTトークンを作成">JWTトークンを作成</h2><p>次にJWTを作ります。テナントIDとアプリケーションIDはテスト環境のEntraIDで生成したものと同じものを使う、あるいは独自に作るでもどちらでもよいですが決めておきます。UUIDの型式です。このサンプルではテナントIDを0000000-0000-0000-0000-000000000000、アプリケーションIDを1111111-1111-1111-1111-111111111111とします。</p>
<p>Entra ID相当のトークンを作りたいので、こんな感じのclaimを用意します。重要な項目は以下の通りです。</p>
<ul>
<li>tid: テナントIDとする</li>
<li>aud: クライアントID＝アプリケーションID</li>
<li>iss: https://login.microsoftonline.com/(テナントID)/v2.0</li>
<li>exp: 有効日時(2035年にしてある)</li>
<li>iat&#x2F;nbf: ログイン日時</li>
<li>exp: 有効期限</li>
<li>sub: ユーザーのキー</li>
</ul>
<div class="code-block"><figure class="highlight json"><input type="checkbox" id="code-wrap-gsrbm7-1" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-gsrbm7-1" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;aud&quot;</span><span class="punctuation">:</span> <span class="string">&quot;1111111-1111-1111-1111-111111111111&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;iss&quot;</span><span class="punctuation">:</span> <span class="string">&quot;https://login.microsoftonline.com/0000000-0000-0000-0000-000000000000/v2.0&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;iat&quot;</span><span class="punctuation">:</span> <span class="number">1671015703</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;nbf&quot;</span><span class="punctuation">:</span> <span class="number">1671015703</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;exp&quot;</span><span class="punctuation">:</span> <span class="number">2071019603</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;idp&quot;</span><span class="punctuation">:</span> <span class="string">&quot;https://sts.windows.net/3eca0868-d511-4342-8659-e88a2e3bf9fe/&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Test User(テストユーザー)&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;nonce&quot;</span><span class="punctuation">:</span> <span class="string">&quot;ce3fd167-0e5a-43ae-bb8b-11d8d003d8c6&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;oid&quot;</span><span class="punctuation">:</span> <span class="string">&quot;daf6a6c6-d549-4421-bf71-3b59fd74d531&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;preferred_username&quot;</span><span class="punctuation">:</span> <span class="string">&quot;test.user@example.co.jp&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;rh&quot;</span><span class="punctuation">:</span> <span class="string">&quot;0.AWoA733wxZg0tkymktoZGQzOJ_1Ryon2gERMsUs1n-XnFcpqAFQ.&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;sub&quot;</span><span class="punctuation">:</span> <span class="string">&quot;oXxd31705vfpTnrSPcdVCdAoalq7ZgQ_gx7Msq7OBzY&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;tid&quot;</span><span class="punctuation">:</span> <span class="string">&quot;0000000-0000-0000-0000-000000000000&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;uti&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Fw7ZoF0z1UCipEF8hfwZAA&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;ver&quot;</span><span class="punctuation">:</span> <span class="string">&quot;2.0&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure></div>

<p>subはユーザーを表すユニークなキーとされていますが、Entra ID上のこのsubと、実際のシステム上のユーザーの対応付けは別途決めておく必要があります。Entra IDとHRのシステムの同期をきちんととっておくのであればこのsubを使ってユーザーを決められますが、そうでない場合はマニフェストを変更してユーザーコード相当の情報を登録したり、preferred_usernameを使うといったことが必要になります。そのあたりはサーバー側の方針に合わせて上記のclaimは修正してください。</p>
<p>次にjwt.ioを開きます。</p>
<p>https://jwt.io/</p>
<p>アルゴリズムをRS256に変更し、PAYLOADに上記のclaimを、その下の署名欄の上の公開鍵にはmkjwk.orgで作った公開鍵を、その下の秘密鍵にはmkjwk.orgで作った秘密鍵を貼り付けます。</p>
<p>これで左側にJWTが生成されます。</p>
<img src="/images/2023/20231227a/image_2.png" alt="image.png" width="1200" height="1334" loading="lazy">

<p>これはmkjws.orgで作成したjwks.orgの証明書を使って署名の確認が行えます。サーバー側でJWTトークンを検証するときは、Entra IDの秘密鍵ではなく、mkjwk.orgで生成した秘密鍵（真ん中のPublic and Private Keypair Setが同じ形式なので扱いやすい）を使って署名の検証が行えます。任意のユーザー情報を作ってトークンを量産できますし、実際のログインは不要なのでテストでも扱いやすいでしょう。このトークンを使えばcurlでもなんでも自由にリクエストが飛ばせるようになります。</p>
]]></content>
    <summary type="html">Entra ID（旧名Azure AD）は企業での利用の割合が高く、社内システムを作る場合はこれを使って認証して欲しいという要件が積まれることがほとんどでしょう。とはいえ、現物を使ってテストを作るのは都合がよくないこともあったりします。</summary>
    <category term="認証認可" scheme="https://future-architect.github.io/categories/%E8%AA%8D%E8%A8%BC%E8%AA%8D%E5%8F%AF/"/>
    <category term="EntraID" scheme="https://future-architect.github.io/tags/EntraID/"/>
    <category term="JWT" scheme="https://future-architect.github.io/tags/JWT/"/>
    <category term="テスト" scheme="https://future-architect.github.io/tags/%E3%83%86%E3%82%B9%E3%83%88/"/>
  </entry>
  <entry>
    <title>MailSlurperを使って6桁のコードの送信コードのテストをする</title>
    <link href="https://future-architect.github.io/articles/20230120a/"/>
    <id>https://future-architect.github.io/articles/20230120a/</id>
    <published>2023-01-19T15:00:00.000Z</published>
    <updated>2023-01-19T15:00:00.000Z</updated>
    <author><name>澁川喜規</name></author>
    <content type="html"><![CDATA[<p>以前、認証ミドルウェアのhankoの紹介の中で、hankoがテストで使っているMailSlurperというメールサーバーが面白いという紹介をしました。</p>
<p>https://future-architect.github.io/articles/20220902a/</p>
<p>テストにおいては、モックは使うものの、モックを差し込むレイヤーはソースコードレベルではなくて、インフラレベルで仕掛ける方がいいよ、というのはほぼコンセンサスとなっていると思います。</p>
<ul>
<li>RDBを使うには、DockerでさっとPostgreSQLを差し込む</li>
<li>フロントエンドからのHTTPの外部サービスを使うには、Mock Service WorkerとかCypressのinterceptを使う</li>
</ul>
<p>もちろん、フレームワークでH2とかSQLiteとかのローカルで簡単に使えるDBMSをサポートしているならそれを使うのも手ですが、ともかく、コードレベルのモックオブジェクトを実装するのはなし、という感じですね。</p>
<p>というのも、やり方を間違えると、モックに対するテストコードになって、コード量のわりに品質があがらないとか、結局実システムの挙動の変化に気づけずに不具合が防止できないとか、モックをコードで作るのはあまりよくないという論調ですね。なるべく上流でモックすれば、そのような問題は減ります。将来的にはモックの挙動が正しいかの検証とかそういうあたりの進化もあるかな、と思いつつ、楽に成果が出るならそちらを今は選択すべきと思います。</p>
<p>メールを送信するシステムにおいても、MailSlurperを使えば良さそうなので試してみました。最近よく見かける、6桁の数字のコードで追加認証するシステムのテストです。</p>
<h2 id="MailSlurper">MailSlurper</h2><p>MailSlurperは、MITライセンスのオープンソースのメールサーバー兼クライアントです。SMTPでメールを受けることができて、ブラウザでそのメールを確認できます。また、REST APIも提供されており、受信したメールをAPIで取り出せます。Go製で軽く、Dockerで気軽に起動できます。</p>
<p>メールボックスは1つで、来たメールはすべて一か所に集まります。ドキュメントを見ると、クライアント証明書をアクセス時に必要という設定ができ、本番環境でも使うことを想定してそうです。ただ、エンドユーザー向けに使うにもメールボックスが1つしかないと不便ですし、受信後のイベント起動とかがないので、バックエンド処理のトリガーにするにも少し心もとありません。今のところはテスト用途がベストかな、と思っています。</p>
<p>GitHubを見てもここしばらくはあまり更新されていないのですが、SMTPは機能的には枯れているので問題ないでしょう。</p>
<h2 id="6桁の数値の生成とセキュリティ">6桁の数値の生成とセキュリティ</h2><p>みなさん、Real World HTTPはすでにご覧になられていると思いますので、お手元の本の「14.8.5　タイムベースワンタイムパスワードアルゴリズム（TOTP）」を見れば詳しいことが書かれているので、詳細については語りませんが、秘密鍵として用意したシークレットをもとに、日時情報を加えて6桁の数値を生成します。Goなら <code>github.com/pquerna/otp/totp</code> パッケージを利用するのが簡単です。</p>
<p>6桁の数値の計算はRFCで決められたアルゴリズムに基づいて行います。高いセキュリティが求められるようなサービスであれば、事前に秘密鍵をGoogle Authenticatorなどのアプリに登録しておき、TOTPのアルゴリズムに従って出力した数値をサーバーに送り、サーバー側でも同じ計算をすることで照合します。秘密鍵そのものは最初の登録時以外はネットワークを流れることがないため、通信経路が安全でなくても比較的安全です。仮に通信が傍受されても、そのコードは30秒（たいていのサービスの場合）しか有効でないからです。</p>
<p>一方で、あまりプロ向けのサービス出ない場合は、同じTOTPのアルゴリズムであっても、別の使い方をします。登録されているメールアドレスやSMS、音声通話で6桁のコードをユーザーに伝え、それをユーザーがサーバー画面で入力して戻すことで照合します。通信経路の傍受に対する強度は同じですが、仮にSIMスワップ攻撃を受けたり、メールサーバーのアカウントがクラックされてアクセスされてしまうと突破できてしまうので、手元のハードウェアに触られなければ安心の前述の方法よりはやや安全性は落ちます（もちろん、秘密鍵をそのデバイスにしか入れていないという前提で）。</p>
<p>後者のような機能を実装するサービスは増えているので、それをMailSlurperを使ってテストしてみます。</p>
<h2 id="シークレットの作成">シークレットの作成</h2><p>シークレットの生成は <code>github.com/pquerna/otp</code> で簡単にできます。シークレット生成はユーザー登録時に行い、サーバー側でユーザーごとに保存します。後半のコードは、すでに登録済みのユーザーに対して行う前提なので、あらかじめ作っておいてテストコードに利用します。登録プロセスを実装する場合はこちらのコードを参考にしてください。</p>
<div class="code-block"><figure class="highlight go"><input type="checkbox" id="code-wrap-got8km-1" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-got8km-1" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">	<span class="string">&quot;fmt&quot;</span></span><br><span class="line">    <span class="string">&quot;log&quot;</span></span><br><span class="line"></span><br><span class="line">	<span class="string">&quot;github.com/pquerna/otp/totp&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    key, err := totp.Generate(totp.GenerateOpts&#123;&#125;)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        log.Fatal(err)</span><br><span class="line">    &#125;</span><br><span class="line">    fmt.Printf(<span class="string">&quot;key: %s\n&quot;</span>, key.Secret())</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div>

<h2 id="テストサーバーの起動">テストサーバーの起動</h2><p>テストのためのMailSlurperを起動しておきます。docker composeを利用します。ウェブの管理画面、API、SMTPポートの3つを開けておきます。なお、公式のDockerイメージはなく、野良イメージが多いのですが、marcopas&#x2F;docker-mailslurper が一番ドキュメントが充実しています。</p>
<figure class="highlight yaml"><figcaption><span>docker-compose.yaml</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="attr">services:</span></span><br><span class="line">  <span class="attr">mailslurper:</span></span><br><span class="line">    <span class="attr">image:</span> <span class="string">marcopas/docker-mailslurper:latest</span></span><br><span class="line">    <span class="attr">ports:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&#x27;8080:8080&#x27;</span> <span class="comment"># web UI</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&#x27;8085:8085&#x27;</span> <span class="comment"># API</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">&#x27;2500:2500&#x27;</span> <span class="comment"># smtp</span></span><br></pre></td></tr></table></figure>

<p>あとは起動するだけです。 http://localhost:8080 にアクセスして管理画面にアクセスできることを確認しましょう。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">docker compose up</span><br></pre></td></tr></table></figure>

<img fetchpriority="high" src="/images/2023/20230120a/スクリーンショット_2023-01-16_1.41.30.png" alt="スクリーンショット_2023-01-16_1.41.30.png" width="1200" height="684">

<h2 id="テストコード作成">テストコード作成</h2><p>これから作るコードは、6桁の認証コードつきのメールを送信するものです。その6桁の数値が正しいものかどうかの検証を来ないます。</p>
<p>MailSlurperはREST APIを提供しています。送信されたメール一覧を取得してきます。取得にあたっては、送信もとアドレスや送信先のアドレスでフィルタリングもできます。</p>
<p>まずはテストヘルパーとして、メールサーバーからメールをとってくるコードを作成してみます。6桁の数値を取り出します。送信先アドレスでフィルタリングを行うようにします。同時にテストを並行で走らせたとしても、送信先のユーザー（アドレス）を分けておけばテストが干渉することがなくなります。今回はGoで実装しています。</p>
<div class="code-block"><figure class="highlight go"><input type="checkbox" id="code-wrap-got8km-2" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-got8km-2" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">package</span> authcode</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">	<span class="string">&quot;encoding/json&quot;</span></span><br><span class="line">	<span class="string">&quot;net/http&quot;</span></span><br><span class="line">	<span class="string">&quot;net/url&quot;</span></span><br><span class="line">	<span class="string">&quot;regexp&quot;</span></span><br><span class="line">	<span class="string">&quot;strings&quot;</span></span><br><span class="line">	<span class="string">&quot;testing&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment">// json2goで作成した、MailSlurperのメールアドレス一覧のレスポンス</span></span><br><span class="line"><span class="keyword">type</span> MailSlurperResponse <span class="keyword">struct</span> &#123;</span><br><span class="line">	MailItems    []MailItem <span class="string">`json:&quot;mailItems&quot;`</span></span><br><span class="line">	TotalPages   <span class="type">int</span>        <span class="string">`json:&quot;totalPages&quot;`</span></span><br><span class="line">	TotalRecords <span class="type">int</span>        <span class="string">`json:&quot;totalRecords&quot;`</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> MailItem <span class="keyword">struct</span> &#123;</span><br><span class="line">	ID          <span class="type">string</span>   <span class="string">`json:&quot;id&quot;`</span></span><br><span class="line">	DateSent    <span class="type">string</span>   <span class="string">`json:&quot;dateSent&quot;`</span></span><br><span class="line">	FromAddress <span class="type">string</span>   <span class="string">`json:&quot;fromAddress&quot;`</span></span><br><span class="line">	ToAddresses []<span class="type">string</span> <span class="string">`json:&quot;toAddresses&quot;`</span></span><br><span class="line">	Subject     <span class="type">string</span>   <span class="string">`json:&quot;subject&quot;`</span></span><br><span class="line">	Body        <span class="type">string</span>   <span class="string">`json:&quot;body&quot;`</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// toアドレスでフィルタリングしてのメールの取り出し</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">ReceiveMail</span><span class="params">(t *testing.T, host, to <span class="type">string</span>)</span></span> []MailItem &#123;</span><br><span class="line">	t.Helper()</span><br><span class="line">	u, err := url.Parse(host)</span><br><span class="line">	<span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">		<span class="built_in">panic</span>(err)</span><br><span class="line">	&#125;</span><br><span class="line">	u.Path = <span class="string">&quot;/mail&quot;</span></span><br><span class="line">	q := url.Values&#123;&#125;</span><br><span class="line">	q.Set(<span class="string">&quot;to&quot;</span>, to)</span><br><span class="line">	u.RawQuery = q.Encode()</span><br><span class="line"></span><br><span class="line">	res, err := http.Get(u.String())</span><br><span class="line">	<span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">		<span class="built_in">panic</span>(err)</span><br><span class="line">	&#125;</span><br><span class="line">	<span class="keyword">defer</span> res.Body.Close()</span><br><span class="line">	d := json.NewDecoder(res.Body)</span><br><span class="line">	r := MailSlurperResponse&#123;&#125;</span><br><span class="line">	err = d.Decode(&amp;r)</span><br><span class="line">	<span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">		<span class="built_in">panic</span>(err)</span><br><span class="line">	&#125;</span><br><span class="line">	<span class="keyword">return</span> r.MailItems</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 6桁のコードを取り出す（裏でメールサーバーから情報取得）</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">ReceivePassCode</span><span class="params">(t *testing.T, host, to <span class="type">string</span>)</span></span> <span class="type">string</span> &#123;</span><br><span class="line">	t.Helper()</span><br><span class="line">	mails := ReceiveMail(t, host, to)</span><br><span class="line"></span><br><span class="line">	p := regexp.MustCompile(<span class="string">`\d&#123;6&#125;`</span>)</span><br><span class="line"></span><br><span class="line">	<span class="keyword">for</span> _, m := <span class="keyword">range</span> mails &#123;</span><br><span class="line">		<span class="keyword">return</span> p.FindString(m.Body)</span><br><span class="line">	&#125;</span><br><span class="line">	<span class="keyword">return</span> <span class="string">&quot;&quot;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div>

<p>完成したテストコードは以下の通りです。短く書けますね。</p>
<div class="code-block"><figure class="highlight go"><input type="checkbox" id="code-wrap-got8km-3" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-got8km-3" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">package</span> authcode</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">	<span class="string">&quot;os&quot;</span></span><br><span class="line">	<span class="string">&quot;testing&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">TestValidate</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">	secret := <span class="string">&quot;LB6BHGYD63JCWM4BBPHCSRBXGZYKGDI3&quot;</span> <span class="comment">// 事前に作成しておいたシークレット</span></span><br><span class="line">    <span class="comment">// これから作成する、パスコード送信処理</span></span><br><span class="line">	err := SendPassCode(<span class="string">&quot;localhost:2500&quot;</span>, <span class="string">&quot;test user&quot;</span>, <span class="string">&quot;test@example.com&quot;</span>, secret)</span><br><span class="line">	<span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">		t.Errorf(<span class="string">&quot;error should be nil: %v&quot;</span>, err)</span><br><span class="line">		<span class="keyword">return</span></span><br><span class="line">	&#125;</span><br><span class="line"></span><br><span class="line">	code := ReceivePassCode(t, <span class="string">&quot;http://localhost:8085&quot;</span>, <span class="string">&quot;test@example.com&quot;</span>)</span><br><span class="line">    <span class="comment">// これから実装するバリデーション</span></span><br><span class="line">	<span class="keyword">if</span> !Validate(code, secret) &#123;</span><br><span class="line">		t.Error(<span class="string">&quot;validation failed&quot;</span>)</span><br><span class="line">	&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div>

<p>APIエンドポイントの<code>/mail</code>に<code>to</code>クエリーをつけて帰ってくるJSONをいじるだけなので、他の言語でもすぐに実装できると思います。</p>
<h2 id="コード生成とメール送信">コード生成とメール送信</h2><p>登録済みのユーザー（サーバーは、名前、メールアドレスおよび、シークレットを知っている）に対して、コードを生成して送信します。なお、レガシーなもろもろの塊であるメールで日本語を正しく送信するにあたっては、以下のQiita記事を参考にしました。</p>
<ul>
<li>go で utf8メールを送信</li>
</ul>
<p>上記のテストが通るように実装したのが以下のテストです。</p>
<div class="code-block"><figure class="highlight go"><input type="checkbox" id="code-wrap-got8km-4" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-got8km-4" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">package</span> authcode</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">	<span class="string">&quot;bytes&quot;</span></span><br><span class="line">	<span class="string">&quot;encoding/base64&quot;</span></span><br><span class="line">	<span class="string">&quot;net/mail&quot;</span></span><br><span class="line">	<span class="string">&quot;net/smtp&quot;</span></span><br><span class="line">	<span class="string">&quot;strings&quot;</span></span><br><span class="line">	<span class="string">&quot;time&quot;</span></span><br><span class="line"></span><br><span class="line">	<span class="string">&quot;github.com/pquerna/otp/totp&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment">// 上記のエントリーから、add76crlf, utf8Split, encodeSubjectをコピーしておくこと</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// メールの作成</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">GenerateMessage</span><span class="params">(toUserName, toAddress, secret <span class="type">string</span>)</span></span> ([]<span class="type">byte</span>, <span class="type">error</span>) &#123;</span><br><span class="line">	from := mail.Address&#123;<span class="string">&quot;Myサービス&quot;</span>, <span class="string">&quot;noreply@my-service.com&quot;</span>&#125;</span><br><span class="line">	to := mail.Address&#123;toUserName, toAddress&#125;</span><br><span class="line"></span><br><span class="line">	<span class="keyword">var</span> msg bytes.Buffer</span><br><span class="line">	msg.WriteString(<span class="string">&quot;From: &quot;</span> + from.String() + <span class="string">&quot;\r\n&quot;</span>)</span><br><span class="line">	msg.WriteString(<span class="string">&quot;To: &quot;</span> + to.String() + <span class="string">&quot;\r\n&quot;</span>)</span><br><span class="line">	msg.WriteString(encodeSubject(<span class="string">&quot;Myサービスの認証コード&quot;</span>))</span><br><span class="line">	msg.WriteString(<span class="string">&quot;MIME-Version: 1.0\r\n&quot;</span>)</span><br><span class="line">	msg.WriteString(<span class="string">&quot;Content-Type: text/plain; charset=\&quot;utf-8\&quot;\r\n&quot;</span>)</span><br><span class="line">	msg.WriteString(<span class="string">&quot;Content-Transfer-Encoding: base64\r\n&quot;</span>)</span><br><span class="line"></span><br><span class="line">	code, err := totp.GenerateCode(secret, time.Now())</span><br><span class="line">	<span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">		<span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line">	&#125;</span><br><span class="line"></span><br><span class="line">	<span class="keyword">var</span> body bytes.Buffer</span><br><span class="line">	body.WriteString(<span class="string">&quot;認証コードはこちらです\n\n&quot;</span> + code + <span class="string">&quot;\n\nMyサービス&quot;</span>)</span><br><span class="line">	msg.WriteString(<span class="string">&quot;\r\n&quot;</span>)</span><br><span class="line">	msg.WriteString(add76crlf(base64.StdEncoding.EncodeToString(body.Bytes())))</span><br><span class="line"></span><br><span class="line">	<span class="keyword">return</span> msg.Bytes(), <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// メール送信</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">SendPassCode</span><span class="params">(host, toUserName, toAddress, secret <span class="type">string</span>)</span></span> <span class="type">error</span> &#123;</span><br><span class="line">	msg, err := GenerateMessage(toUserName, toAddress, secret)</span><br><span class="line">	<span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">		<span class="keyword">return</span> err</span><br><span class="line">	&#125;</span><br><span class="line">	err = smtp.SendMail(</span><br><span class="line">		host,</span><br><span class="line">		<span class="literal">nil</span>,</span><br><span class="line">		<span class="string">&quot;noreply@my-service.com&quot;</span>,</span><br><span class="line">		[]<span class="type">string</span>&#123;toAddress&#125;,</span><br><span class="line">		msg,</span><br><span class="line">	)</span><br><span class="line">	<span class="keyword">return</span> err</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// クライアントに送信されたパスコードのバリデーションを行う</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Validate</span><span class="params">(passcode, secret <span class="type">string</span>)</span></span> <span class="type">bool</span> &#123;</span><br><span class="line">	<span class="keyword">return</span> totp.Validate(passcode, secret)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div>

<p>先ほどのテストに対して実行すると、正しくテストをパスします。簡単にメール送信を伴うコードのテストができました。</p>
<h2 id="テストの後始末">テストの後始末</h2><p>テストを行い続けると、メールボックスにメールが溜まり続けます。リソースを消費する量は大したことがないとはいえ、増え続けるのは精神衛生上良くないです。幸い、MailSlurperはメールボックスのリセットもAPIで提供してくれていますので、それを使ってみます。</p>
<p>まずは先ほどのテストヘルパーのファイルに以下のメールボックスリセットを送信するヘルパー関数を追加します。</p>
<div class="code-block"><figure class="highlight go"><input type="checkbox" id="code-wrap-got8km-5" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-got8km-5" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">ResetMailSlumper</span><span class="params">(host <span class="type">string</span>)</span></span> &#123;</span><br><span class="line">	u, err := url.Parse(host)</span><br><span class="line">	<span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">		<span class="built_in">panic</span>(err)</span><br><span class="line">	&#125;</span><br><span class="line">	u.Path = <span class="string">&quot;/mail&quot;</span></span><br><span class="line"></span><br><span class="line">	req, _ := http.NewRequest(<span class="string">&quot;DELETE&quot;</span>, u.String(), strings.NewReader(<span class="string">`&#123;&quot;pruneCode&quot;: &quot;all&quot;&#125;`</span>))</span><br><span class="line">	req.Header.Set(<span class="string">&quot;Content-Type&quot;</span>, <span class="string">&quot;application/json&quot;</span>)</span><br><span class="line">	http.DefaultClient.Do(req)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div>

<p>テストの実行前にリセットを呼ぶようにします。後始末だと、実行後の方が自然に思えるかもしれませんが、テストのリソースのリセットを後にしてしまうと、問題発生時に結果を追いかけるのが大変になるため、僕は全体の実行前にクリアするようにしています。</p>
<figure class="highlight go"><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">TestMain</span><span class="params">(m *testing.M)</span></span> &#123;</span><br><span class="line">	ResetMailSlumper(<span class="string">&quot;http://localhost:8085&quot;</span>)</span><br><span class="line">	code := m.Run()</span><br><span class="line">	os.Exit(code)</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure>

<h2 id="まとめ">まとめ</h2><p>これで実SMTPサーバーを使ったコードを書いて、それをMailSlurperを使ってテストする方法を学びました。REST APIのおかげで、ヘルパーさえ用意してしまえば、テストを書くのは簡単です。</p>
<p>これだけ使いやすいとなると、非同期通信系は全部SMTPに寄せたくなってくる気もします。まあ本番環境の安定稼働を考えると実際にやることはないですが、MailSlurperは送信結果を見るのもできて、開発体験はかなり良いです。</p>
]]></content>
    <summary type="html">以前、認証ミドルウェアのhankoの紹介の中で、hankoがテストで使っているMailSlurperというメールサーバーが面白いという紹介をしました。テストにおいては、モックは使うものの、モックを差し込むレイヤーはソースコードレベルではなくて、インフラレベルで仕掛ける方がいいよ、というのはほぼコンセンサスとなっていると思います。</summary>
    <category term="認証認可" scheme="https://future-architect.github.io/categories/%E8%AA%8D%E8%A8%BC%E8%AA%8D%E5%8F%AF/"/>
    <category term="Go" scheme="https://future-architect.github.io/tags/Go/"/>
    <category term="MailSlurper" scheme="https://future-architect.github.io/tags/MailSlurper/"/>
    <category term="テスト" scheme="https://future-architect.github.io/tags/%E3%83%86%E3%82%B9%E3%83%88/"/>
    <category term="メール" scheme="https://future-architect.github.io/tags/%E3%83%A1%E3%83%BC%E3%83%AB/"/>
  </entry>
  <entry>
    <title>GKEでIdentity-Aware Proxyを利用したWebアプリケーション認証</title>
    <link href="https://future-architect.github.io/articles/20230113a/"/>
    <id>https://future-architect.github.io/articles/20230113a/</id>
    <published>2023-01-12T15:00:00.000Z</published>
    <updated>2023-01-12T15:00:00.000Z</updated>
    <author><name>渡邉光</name></author>
    <content type="html"><![CDATA[<h2 id="はじめに">はじめに</h2><p>明けましておめでとうございます！ Future筋肉エンジニアの渡邉です。年も明けたことなので切り替えて減量に入りました。三月末までを目安に体を絞ろうと思っています。</p>
<p>私は現在Google Cloudを利用しているプロジェクトに所属しており、Google Cloudのスキルアップにいそしんでいます。今回はGKE (Google Kubernetes Engine)でCloud IAP (Identity-Aware Proxy)を利用したWebアプリケーションのGoogleアカウント認証について記事を書こうと思います。</p>
<h2 id="Identity-Aware-Proxyとは">Identity-Aware Proxyとは</h2><p>以下、公式ドキュメント引用</p>
<blockquote>
<p>IAP を使用すると、HTTPS によってアクセスされるアプリケーションの一元的な承認レイヤを確立できるため、ネットワーク レベルのファイアウォールに頼らずに、アプリケーション レベルのアクセス制御モデルを使用できます。</p>
</blockquote>
<p>簡単に言うとGoogleアカウントとCloud IAMの仕組みを用いてWebアプリケーションの認証をできます。</p>
<h3 id="認証・承認フロー">認証・承認フロー</h3><img fetchpriority="high" src="/images/2023/20230113a/authenticate-flow.drawio.png" alt="authenticate-flow.drawio.png" width="487" height="564">

<p>公式ドキュメントはこちら</p>
<ul>
<li>Google Cloudリソースへのリクエスト(Cloud Load Balancing)します。</li>
<li>IAPが有効になっている場合は、IAP認証サーバへ情報を送信します（プロジェクト番号、リクエストURL、リクエストヘッダー、Cookie内のIAP認証情報など）</li>
<li>IAP認証サーバがブラウザの認証情報をチェックします。</li>
<li>認証情報が存在しない場合は、OAuth2.0のGoogleアカウントログインフローにリダイレクトし、認証確認を実施する。認証トークンは今後のアクセスのためブラウザのCookieに保存されます。</li>
<li>認証情報が有効な場合、認証サーバは認証情報からユーザのID（メールアドレスとユーザID）を取得します。</li>
<li>認証サーバはこのIDからユーザのIAMロールをチェックし、ユーザがリソースにアクセスできる権限(<strong>IAP で保護されたウェブアプリ ユーザー</strong>)を持っているかをチェックします</li>
<li>権限を持っていれば、アクセスOKになり、なければNGになります。</li>
</ul>
<h2 id="全体アーキテクチャ図">全体アーキテクチャ図</h2><p>以下が全体アーキテクチャ図になります。<br>GKE&#x2F;NetworkなどのGoogle Cloudのリソース構築に関しては慣れ親しんでいるTerraformを利用して作成しました。OAuth同意画面に関しては外部公開する場合は、APIから作成できない (公式ドキュメント記載)ので、コンソール画面から設定しました。</p>
<img src="/images/2023/20230113a/architecture.drawio.png" alt="architecture.drawio.png" width="1151" height="429" loading="lazy">

<h3 id="Bastion初期設定">Bastion初期設定</h3><p>Public Subnetに作成したGCEインスタンスからGKEのコントロールプレーンに対してkubectlコマンドを実行したいので、<br>kubectlコマンドや、google-cloud-sdk-gke-gcloud-auth-pluginなどをインストールします。<br>以下、Bashスクリプトです。</p>
<div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-1qjjblr-1" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1qjjblr-1" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="meta">#!/bin/bash</span></span><br><span class="line"></span><br><span class="line"><span class="comment">########################################################</span></span><br><span class="line"><span class="comment"># Author: watanabe</span></span><br><span class="line"><span class="comment"># Initial Date: 2022/12/28</span></span><br><span class="line"><span class="comment"># History: Create</span></span><br><span class="line"><span class="comment">########################################################</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Variable Definition</span></span><br><span class="line">project_name=<span class="string">&quot;xxxxxxxxxx&quot;</span></span><br><span class="line">gke_cluster_name=<span class="string">&quot;xxxxxxxxx&quot;</span></span><br><span class="line">region=<span class="string">&quot;asia-northeast1&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Install Kubectl</span></span><br><span class="line">curl -LO <span class="string">&quot;https://storage.googleapis.com/kubernetes-release/release/<span class="subst">$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)</span>/bin/linux/amd64/kubectl&quot;</span></span><br><span class="line"><span class="built_in">chmod</span> +x ./kubectl</span><br><span class="line"><span class="built_in">sudo</span> <span class="built_in">mv</span> ./kubectl /usr/local/bin/kubectl</span><br><span class="line">kubectl version</span><br><span class="line"></span><br><span class="line"><span class="comment"># Install google-cloud-sdk-gke-gcloud-auth-plugin</span></span><br><span class="line"><span class="built_in">sudo</span> apt-get install apt-transport-https ca-certificates gnupg</span><br><span class="line"><span class="built_in">echo</span> <span class="string">&quot;deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main&quot;</span> | <span class="built_in">sudo</span> <span class="built_in">tee</span> -a /etc/apt/sources.list.d/google-cloud-sdk.list</span><br><span class="line">curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | <span class="built_in">sudo</span> apt-key --keyring /usr/share/keyrings/cloud.google.gpg add -</span><br><span class="line"><span class="built_in">sudo</span> apt-get update &amp;&amp; <span class="built_in">sudo</span> apt-get install google-cloud-cli</span><br><span class="line"><span class="built_in">sudo</span> apt-get install google-cloud-sdk-gke-gcloud-auth-plugin</span><br><span class="line">gke-gcloud-auth-plugin --version</span><br><span class="line"><span class="built_in">export</span> USE_GKE_GCLOUD_AUTH_PLUGIN=True</span><br><span class="line"><span class="built_in">source</span> ~/.bashrc</span><br><span class="line"></span><br><span class="line"><span class="comment"># Get Credentials</span></span><br><span class="line">gcloud container clusters get-credentials <span class="string">&quot;<span class="variable">$&#123;gke_cluster_name&#125;</span>&quot;</span> --region <span class="string">&quot;<span class="variable">$&#123;region&#125;</span>&quot;</span> --project <span class="string">&quot;<span class="variable">$&#123;project_name&#125;</span>&quot;</span></span><br><span class="line">kubectl config get-contexts</span><br><span class="line">kubectl get node</span><br></pre></td></tr></table></figure></div>

<h3 id="manifestファイル">manifestファイル</h3><p>また、manifestファイルは以下を用意してkubectlコマンドを実行しk8sリソースをGKEに対して作成しました。</p>
<p>ここまでの設定で事前準備は完了です。</p>
<h4 id="Deployment">Deployment</h4><p>nginxのPodを用意するため、Deploymentのmanifestを作成しました。</p>
<figure class="highlight yaml"><figcaption><span>deployment.yaml</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="attr">apiVersion:</span> <span class="string">apps/v1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">Deployment</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">nginx-deployment</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line">  <span class="attr">selector:</span></span><br><span class="line">    <span class="attr">matchLabels:</span></span><br><span class="line">      <span class="attr">app:</span> <span class="string">nginx</span></span><br><span class="line">  <span class="attr">replicas:</span> <span class="number">3</span></span><br><span class="line">  <span class="attr">template:</span></span><br><span class="line">    <span class="attr">metadata:</span></span><br><span class="line">      <span class="attr">labels:</span></span><br><span class="line">        <span class="attr">app:</span> <span class="string">nginx</span></span><br><span class="line">    <span class="attr">spec:</span></span><br><span class="line">      <span class="attr">containers:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">nginx</span></span><br><span class="line">        <span class="attr">image:</span> <span class="string">nginx:1.22</span></span><br><span class="line">        <span class="attr">ports:</span></span><br><span class="line">        <span class="bullet">-</span> <span class="attr">containerPort:</span> <span class="number">80</span></span><br></pre></td></tr></table></figure>

<h4 id="Service">Service</h4><p>IngressにはNodePortが必要になるので、Serviceのmanifestを作成しました。</p>
<figure class="highlight yaml"><figcaption><span>service.yaml</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">Service</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">nginx-service</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line">  <span class="attr">type:</span> <span class="string">NodePort</span></span><br><span class="line">  <span class="attr">selector:</span></span><br><span class="line">    <span class="attr">app:</span> <span class="string">nginx</span></span><br><span class="line">  <span class="attr">ports:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">port:</span> <span class="number">80</span></span><br><span class="line">      <span class="attr">targetPort:</span> <span class="number">80</span></span><br><span class="line">      <span class="attr">protocol:</span> <span class="string">TCP</span></span><br></pre></td></tr></table></figure>

<h4 id="ManagedCertificate">ManagedCertificate</h4><p>クライアントとIngressで構築するHTTP(S)ロードバランサ間をHTTPSでアクセスするようにしたいので、Googleマネージド証明書のmanifestを作成しました。<br>domainsには、terraformで用意したHTTP(S)ロードバランサに設定したい外部IPアドレスにフリーなワイルドカードDNSサービスのnip.ioを利用したものを設定します。</p>
<figure class="highlight yaml"><figcaption><span>managed-certificate.yaml</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="attr">apiVersion:</span> <span class="string">networking.gke.io/v1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">ManagedCertificate</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">nginx</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line">  <span class="attr">domains:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="number">34.</span><span class="string">xxx.xxx.xxx.nip.io</span></span><br></pre></td></tr></table></figure>

<h4 id="Ingress">Ingress</h4><p>インターネット上にnginxを公開するためにIngressを構築するmanifestを作成しました。</p>
<div class="code-block"><figure class="highlight yaml"><input type="checkbox" id="code-wrap-1qjjblr-2" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1qjjblr-2" title="コードの折り返しを切り替える"></label><figcaption><span>ingress.yaml</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="attr">apiVersion:</span> <span class="string">networking.k8s.io/v1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">Ingress</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">nginx-ingress</span></span><br><span class="line">  <span class="attr">annotations:</span></span><br><span class="line">    <span class="comment"># 外部ロードバランサの作成</span></span><br><span class="line">    <span class="attr">kubernetes.io/ingress.class:</span> <span class="string">&quot;gce&quot;</span></span><br><span class="line">    <span class="comment"># クライアントとHTTP(S)ロードバランサ間のすべての通信をHTTPSに強制</span></span><br><span class="line">    <span class="attr">kubernetes.io/ingress.allow-http:</span> <span class="string">&quot;false&quot;</span></span><br><span class="line">    <span class="comment"># 事前に用意していた静的外部IPアドレスを設定する</span></span><br><span class="line">    <span class="attr">kubernetes.io/ingress.global-static-ip-name:</span> <span class="string">&quot;loadbalancer-external-ip-address&quot;</span></span><br><span class="line">    <span class="comment"># Googleマネージド証明書をIngressに適用する</span></span><br><span class="line">    <span class="attr">networking.gke.io/managed-certificates:</span> <span class="string">&quot;nginx&quot;</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line">  <span class="attr">rules:</span></span><br><span class="line">  <span class="bullet">-</span> <span class="attr">http:</span></span><br><span class="line">      <span class="attr">paths:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="attr">path:</span> <span class="string">/</span></span><br><span class="line">        <span class="attr">pathType:</span> <span class="string">Prefix</span></span><br><span class="line">        <span class="attr">backend:</span></span><br><span class="line">          <span class="attr">service:</span></span><br><span class="line">            <span class="attr">name:</span> <span class="string">nginx-service</span></span><br><span class="line">            <span class="attr">port:</span></span><br><span class="line">              <span class="attr">number:</span> <span class="number">80</span></span><br></pre></td></tr></table></figure></div>

<h2 id="Cloud-IAPなしでのアクセス確認">Cloud IAPなしでのアクセス確認</h2><p>まず、Cloud IAPなしでのアクセス確認を行います。<br>Load Balancerに設定したドメインに対してアクセスを行うと、特に認証画面を経由することもなくアクセスできます。<br><img src="/images/2023/20230113a/1-IAPなしでのアクセス確認.png" alt="1-IAPなしでのアクセス確認.png" width="956" height="525" loading="lazy"></p>
<h2 id="Cloud-IAPの設定を追加">Cloud IAPの設定を追加</h2><p>上記の状態ではだれでもアクセスできるため、セキュアな状態ではありません。<br>ここでCloud IAPの設定を追加してみましょう。</p>
<h3 id="OAuth同意画面の作成">OAuth同意画面の作成</h3><p>OAuth同意画面はUser Typeを「外部」で作成します。<br><img src="/images/2023/20230113a/2-OAuth同意画面①.png" alt="2-OAuth同意画面①.png" width="1200" height="848" loading="lazy"></p>
<p>アプリ情報として、必須項目の以下を設定して「保存して次へ」をクリックします。<br>ほかの情報は任意のため設定しませんでした。</p>
<ul>
<li>アプリ名：GKE Application</li>
<li>ユーザサポートメール：自身のメールアドレス</li>
<li>デベロッパーの連絡先情報：自身のメールアドレス</li>
</ul>
<img src="/images/2023/20230113a/2-OAuth同意画面②.png" alt="2-OAuth同意画面②.png" width="1200" height="838" loading="lazy">

<img src="/images/2023/20230113a/2-OAuth同意画面③.png" alt="2-OAuth同意画面③.png" width="1200" height="843" loading="lazy">

<p>スコープとテストユーザは任意情報のため設定しませんでした。<br>以下が設定完了したOAuth同意画面になります。</p>
<img src="/images/2023/20230113a/2-OAuth同意画面④.png" alt="2-OAuth同意画面④.png" width="1200" height="844" loading="lazy">

<h3 id="OAuth認証情報の作成">OAuth認証情報の作成</h3><p>APIとサービスタブの「認証情報」をクリックします。<br>認証情報の作成プルダウンリストからOAuthクライアントIDをクリックします。</p>
<img src="/images/2023/20230113a/3-OAuth認証情報①.png" alt="3-OAuth認証情報①.png" width="1200" height="843" loading="lazy">

<ul>
<li>アプリケーションの種類：ウェブアプリケーション</li>
<li>OAuthクライアントIDの名前：GKE Application<br>を入力し、作成ボタンをクリックします。</li>
</ul>
<img src="/images/2023/20230113a/3-OAuth認証情報②.png" alt="3-OAuth認証情報②.png" width="1200" height="851" loading="lazy">

<p>作成ボタンをクリックするとOAuthクライアントIDとクライアントシークレットが生成されるので、JSONをダウンロードします。</p>
<img src="/images/2023/20230113a/3-OAuth認証情報③.png" alt="3-OAuth認証情報③.png" width="512" height="448" loading="lazy">

<p>作成したOAuthクライアントを再度クリックし、承認済みリダイレクトURIをダウンロードしたOAuthクライアントID(CLIENT_ID)に修正して保存します。</p>
<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-1qjjblr-3" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1qjjblr-3" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">https://iap.googleapis.com/v1/oauth/clientIds/CLIENT_ID:handleRedirect</span><br></pre></td></tr></table></figure></div>

<img src="/images/2023/20230113a/3-OAuth認証情報④.png" alt="3-OAuth認証情報④.png" width="1200" height="795" loading="lazy">

<h3 id="IAPアクセス権の設定">IAPアクセス権の設定</h3><p>Google Cloud ConsoleのIdentity-Aware Proxyにアクセスします。<br>アクセス権を付与するリソースの横にあるチェックボックスをオンにします。</p>
<img src="/images/2023/20230113a/4-CloudIAPアクセス権設定①.png" alt="4-CloudIAPアクセス権設定①.png" width="1200" height="845" loading="lazy">

<p>IAPの有効化で「構成要件」を参照し、問題なければ「有効にする」をクリックします。<br><img src="/images/2023/20230113a/4-CloudIAPアクセス権設定②.png" alt="4-CloudIAPアクセス権設定②.png" width="564" height="355" loading="lazy"></p>
<p>チェックボックスが「オン」になりました<br>右側のパネルから、「プリンシパルの追加」をクリックします。<br><img src="/images/2023/20230113a/4-CloudIAPアクセス権設定③.png" alt="4-CloudIAPアクセス権設定③.png" width="1200" height="849" loading="lazy"></p>
<p>IAPアクセスを許可したいGoogleアカウント（メールアドレス）または、Googleグループなどを指定して、IAMロール（IAP-secured Web App User）を付与してください。</p>
<img src="/images/2023/20230113a/4-CloudIAPアクセス権設定④.png" alt="4-CloudIAPアクセス権設定④.png" width="736" height="727" loading="lazy">

<p>ここまででOAuthの設定は完了です。</p>
<h3 id="Kubernetes-Secretの作成">Kubernetes Secretの作成</h3><p>GKEでCloud IAPを適用するためには、Kubernetes Secretを作成してBackendConfigに適用する必要があります。<br>先ほど作成してダウンロードしたOAuth認証情報のClient IDとClient Secretを指定してKubernetes Secretを作成します。</p>
<div class="code-block"><figure class="highlight bash"><input type="checkbox" id="code-wrap-1qjjblr-4" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1qjjblr-4" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">kubectl create secret generic oauth-secret --from-literal=client_id=xxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com \</span><br><span class="line">    --from-literal=client_secret=xxxxxxxxxxxxxxxxxxxxxxxxx</span><br></pre></td></tr></table></figure></div>

<p>Kubernetes Secretが作成されていることを確認します。</p>
<div class="code-block"><figure class="highlight console"><input type="checkbox" id="code-wrap-1qjjblr-5" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1qjjblr-5" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">xxxxxxxxxxxxx@tky-bastion:~$ kubectl describe secret oauth-secret</span><br><span class="line">Name:         oauth-secret</span><br><span class="line">Namespace:    default</span><br><span class="line">Labels:       &lt;none&gt;</span><br><span class="line">Annotations:  &lt;none&gt;</span><br><span class="line"></span><br><span class="line">Type:  Opaque</span><br><span class="line"></span><br><span class="line">Data</span><br><span class="line">====</span><br><span class="line">client_secret:  35 bytes</span><br><span class="line">client_id:      73 bytes</span><br></pre></td></tr></table></figure></div>

<h3 id="BackendConfigの作成">BackendConfigの作成</h3><p>Kubernetes Secretで作成したSecretをBackendConfigに設定することでCloud IAPを適用できます。<br>以下のmanifestファイルを用意します。</p>
<figure class="highlight yaml"><figcaption><span>backendconfig.yaml</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="attr">apiVersion:</span> <span class="string">cloud.google.com/v1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">BackendConfig</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">iap-conifg</span></span><br><span class="line">  <span class="attr">namespace:</span> <span class="string">default</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line">  <span class="attr">iap:</span></span><br><span class="line">    <span class="attr">enabled:</span> <span class="literal">true</span></span><br><span class="line">    <span class="attr">oauthclientCredentials:</span></span><br><span class="line">      <span class="attr">secretName:</span> <span class="string">oauth-secret</span></span><br></pre></td></tr></table></figure>

<p>kubectlコマンドでBackendConfigを作成します。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">kubectl apply -f backendconfig.yaml</span><br></pre></td></tr></table></figure>

<p>BackendConfigが作成されていることを確認します。</p>
<div class="code-block"><figure class="highlight console"><input type="checkbox" id="code-wrap-1qjjblr-6" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1qjjblr-6" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">xxxxxxxxxxxxx@tky-bastion:~/manifest$ kubectl get backendconfig</span><br><span class="line">NAME         AGE</span><br><span class="line">iap-conifg   3m42s</span><br></pre></td></tr></table></figure></div>

<p>サービスポートを BackendConfig に関連付けて、IAP の有効化をトリガーする必要があります。既存のService リソースにアノテーションを追加し、サービスのすべてのポートをデフォルトで BackendConfig にします。</p>
<div class="code-block"><figure class="highlight yaml"><input type="checkbox" id="code-wrap-1qjjblr-7" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1qjjblr-7" title="コードの折り返しを切り替える"></label><figcaption><span>service.yaml</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">Service</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">nginx-service</span></span><br><span class="line"><span class="comment"># 追記</span></span><br><span class="line">  <span class="attr">annotations:</span></span><br><span class="line">    <span class="attr">beta.cloud.google.com/backend-config:</span> <span class="string">&#x27;&#123;&quot;default&quot;: &quot;config-default&quot;&#125;&#x27;</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line">  <span class="attr">type:</span> <span class="string">NodePort</span></span><br><span class="line">  <span class="attr">selector:</span></span><br><span class="line">    <span class="attr">app:</span> <span class="string">nginx</span></span><br><span class="line">  <span class="attr">ports:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">port:</span> <span class="number">80</span></span><br><span class="line">      <span class="attr">targetPort:</span> <span class="number">80</span></span><br><span class="line">      <span class="attr">protocol:</span> <span class="string">TCP</span></span><br></pre></td></tr></table></figure></div>

<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">kubectl apply -f service.yaml</span><br></pre></td></tr></table></figure>

<p>以上で、Cloud IAPの設定は完了です。</p>
<h2 id="Cloud-IAPありでのアクセス確認">Cloud IAPありでのアクセス確認</h2><p>Cloud IAPの設定が完了したので、画面にアクセスしてCloud IAPが適用されているかを確認します。</p>
<h3 id="Cloud-IAP認証対象外アカウントでのアクセス確認">Cloud IAP認証対象外アカウントでのアクセス確認</h3><p>Load Balancerに設定したドメインに対してアクセスを行うと、Cloud IAPによるGoogleアカウントログイン画面にリダイレクトされます。</p>
<img src="/images/2023/20230113a/5-IAPアクセスなし①.png" alt="5-IAPアクセスなし①.png" width="469" height="557" loading="lazy">

<p>本GoogleアカウントはCloud IAPのアクセスできる権限(<strong>IAP で保護されたウェブアプリ ユーザー</strong>)を持っていないため、画面にアクセスできません。<br><img src="/images/2023/20230113a/5-IAPアクセスなし②.png" alt="5-IAPアクセスなし②.png" width="426" height="455" loading="lazy"></p>
<h3 id="Cloud-IAP認証対象アカウントでのアクセス確認">Cloud IAP認証対象アカウントでのアクセス確認</h3><p>Load Balancerに設定したドメインに対してアクセスを行うと、Cloud IAPによるGoogleアカウントログイン画面にリダイレクトされます。</p>
<img src="/images/2023/20230113a/6-IAPアクセスあり①.png" alt="6-IAPアクセスあり①.png" width="529" height="565" loading="lazy">

<p>本GoogleアカウントはCloud IAPのアクセスできる権限(<strong>IAP で保護されたウェブアプリ ユーザー</strong>)を持っているため、画面にアクセスできました。<br><img src="/images/2023/20230113a/6-IAPアクセスあり②.png" alt="6-IAPアクセスあり②.png" width="908" height="299" loading="lazy"></p>
<h2 id="さいごに">さいごに</h2><p>今回はGKE (Google Kubernetes Engine)でCloud IAP (Identity-Aware Proxy)を利用したGoogleアカウント認証について記事を書きました。<br>Google Cloudを利用していて、特定のGoogleアカウントにのみアクセスを許可したいケースはあるかと思いますので、その時にでも参考にしていただければ幸いです。</p>
]]></content>
    <summary type="html">GKE を利用したWebアプリケーションのGoogleアカウント認証について記事を書きます。公式ドキュメントを引用します。IAP を使用すると、HTTPS によってアクセスされるアプリケーションの一元的な承認レイヤを確立できるため、ネットワーク レベルのファイアウォールに頼らずに、アプリケーション レベルのアクセス制御モデルを使用できます。</summary>
    <category term="認証認可" scheme="https://future-architect.github.io/categories/%E8%AA%8D%E8%A8%BC%E8%AA%8D%E5%8F%AF/"/>
    <category term="GKE" scheme="https://future-architect.github.io/tags/GKE/"/>
    <category term="GoogleCloud" scheme="https://future-architect.github.io/tags/GoogleCloud/"/>
  </entry>
  <entry>
    <title>MSAL.jsで開発時は認証スキップしたい</title>
    <link href="https://future-architect.github.io/articles/20221220a/"/>
    <id>https://future-architect.github.io/articles/20221220a/</id>
    <published>2022-12-19T15:00:00.000Z</published>
    <updated>2022-12-19T15:00:00.000Z</updated>
    <author><name>澁川喜規</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2022/20221220a/azuread.jpg" alt="" width="700" height="298">

<p>MSAL.jsはとても便利なライブラリです。前に書いたエントリーで説明しましたが、AzureAD側の設定は必要ですが、コードへの組み込みもすぐです。コールバックを受けるバックエンドサーバーの用意も不要で、フロントエンドだけで認証が完結します。</p>
<ul>
<li>https://future-architect.github.io/articles/20221118a/</li>
</ul>
<p>ですが、開発時にAzureADがない場合もありますし、開発者全員が開発で使うAzureADにユーザー登録されていないかもしれません。また、権限ごとにいろんなユーザーを用意してテストできるようにしたいとかのニーズもあると思います。E2Eテストで毎回認証をすると遅いとか、コールバックを受けるコードがGitHub Actionsではうまく動かず実AzureAD認証を組み込むのが難しいとか、認証をスキップしたいニーズもいろいろあるため、開発時にはMSAL.jsをスキップできるようにしてみます。</p>
<h2 id="設定の外だし">設定の外だし</h2><p>前回はハードコードしましたが、AzureADの接続情報などは.envで設定を流し込むべきですので、別ファイルに切り出します。各フレームワークごとに、ブラウザに環境変数を公開するには、キーの名前のルールがあります。Vue.jsであればVUE_APP_を前につけますし、Vite.jsだとVITE_をつけますし、Next.jsだとNEXT_PUBLIC_ですね。これらの設定はサーバーではなくてフロントエンド側なので、それらのルールに従った名前にします。Vue.jsだったら次の通り。</p>
<div class="code-block"><figure class="highlight bash"><input type="checkbox" id="code-wrap-93spou-1" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-93spou-1" title="コードの折り返しを切り替える"></label><figcaption><span>.env</span></figcaption><table><tr><td class="code"><pre><span class="line">VUE_APP_AZURE_DUMMY_USER=dummy-user@example.com</span><br><span class="line">VUE_APP_AZURE_ISSUER=https://login.microsoftonline.com/<span class="variable">$&#123;テナントID&#125;</span>,</span><br><span class="line">VUE_APP_AZURE_APP_ID=<span class="variable">$&#123;アプリケーションID&#125;</span></span><br></pre></td></tr></table></figure></div>

<p>これらの設定を使うようにします。コールバックのURLは現在実行中のホストの<code>/callback</code>を向くように動的にパスを作っています。このパスをAzureAD側の設定にも入れる想定です。</p>
<div class="code-block"><figure class="highlight ts"><input type="checkbox" id="code-wrap-93spou-2" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-93spou-2" title="コードの折り返しを切り替える"></label><figcaption><span>authConfig.ts</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="keyword">type</span> &#123; <span class="title class_">Configuration</span> &#125; <span class="keyword">from</span> <span class="string">&quot;@azure/msal-browser&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="attr">config</span>: <span class="title class_">Configuration</span> = &#123;</span><br><span class="line">    <span class="attr">auth</span>: &#123;</span><br><span class="line">        <span class="attr">authority</span>: process.<span class="property">env</span>.<span class="property">VUE_APP_AZURE_ISSUER</span>,</span><br><span class="line">        <span class="attr">clientId</span>: process.<span class="property">env</span>.<span class="property">VUE_APP_AZURE_APP_ID</span>,</span><br><span class="line">        <span class="attr">redirectUri</span>: <span class="string">`<span class="subst">$&#123;location.protocol&#125;</span>//<span class="subst">$&#123;location.host&#125;</span>/callback`</span></span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">cache</span>: &#123;</span><br><span class="line">        <span class="attr">cacheLocation</span>: <span class="string">&quot;localStorage&quot;</span>,</span><br><span class="line">        <span class="attr">storeAuthStateInCookie</span>: <span class="literal">false</span>,</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure></div>

<h2 id="テストユーザー対応">テストユーザー対応</h2><p>MSAL.jsを使うと、AzureADで認証してJWTトークンを作って返してくれます。それをそのままサーバーにも渡し、サーバー側でIDを取り出して使います。開発用モードを作るとして大幅なif分岐などは作りたくはないですよね？</p>
<ul>
<li>ダミーのJWTは作り、IDが分かるようにする</li>
<li>ただしAzureADの証明書での署名はできないので、署名の確認はサーバーではあきらめる</li>
</ul>
<p>AzureADのトークンを使う場合</p>
<p>今回は開発用のテストユーザーを環境変数から設定できるようにします。</p>
<figure class="highlight bash"><figcaption><span>.env.development</span></figcaption><table><tr><td class="code"><pre><span class="line">VUE_APP_AZURE_DUMMY_USER=dummy-user@example.com</span><br></pre></td></tr></table></figure>

<p>ブラウザ上でダミーのJWTを作るためにjoseパッケージを使います。これはブラウザで使えますが、npmパッケージのほとんどはNode.jsの機能を使っていてブラウザで使えないものが多かったです。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">npm install jose</span><br></pre></td></tr></table></figure>

<p>前回と違うところを主にサンプルとして提示しています。</p>
<p>ログインではダミーユーザーがあるかどうかで条件判断し、ダミーユーザーがいたらJWTを作って返しています。内容はだいたいAzureADが作っているものに似せるようにはしています（完全ではない）。</p>
<p>AzureADのトークンはsubではUUIDのようなコードが入っています。おそらくサーバー側でログインしたユーザーのIDをもとに権限管理をしたりするのであれば、<code>preferred_username</code>に入っているメールアドレスを使うことになるんじゃないかと思います。AzureAD側の設定でIDトークンに入れるクレームを増やして、<code>email</code>クレームを足したりもできるようです。</p>
<div class="code-block"><figure class="highlight ts"><input type="checkbox" id="code-wrap-93spou-3" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-93spou-3" title="コードの折り返しを切り替える"></label><figcaption><span>authPlugin</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="comment">// 追加</span></span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">UnsecuredJWT</span> &#125; <span class="keyword">from</span> <span class="string">&#x27;jose&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// 前回はaccessTokenだったがidTokenに変更</span></span><br><span class="line"><span class="keyword">let</span> idToken = <span class="string">&quot;&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 前回のloginメソッドの修正</span></span><br><span class="line"><span class="keyword">async</span> <span class="title function_">login</span> () &#123;</span><br><span class="line">  <span class="keyword">if</span> (process.<span class="property">env</span>.<span class="property">VUE_APP_AZURE_DUMMY_USER</span>) &#123; <span class="comment">// ダミーユーザーモード</span></span><br><span class="line">    <span class="keyword">const</span> jwt = <span class="keyword">await</span> <span class="keyword">new</span> <span class="title class_">UnsecuredJWT</span>(&#123;</span><br><span class="line">      <span class="attr">idp</span>: <span class="string">&#x27;https://sts.windows.net/....&#x27;</span>,</span><br><span class="line">      <span class="attr">name</span>: <span class="string">&#x27;Dummy User(ダミー ユーザー)&#x27;</span>,</span><br><span class="line">      <span class="attr">preferred_username</span>: process.<span class="property">env</span>.<span class="property">VUE_APP_AZURE_DUMMY_USER</span>,</span><br><span class="line">      <span class="attr">sub</span>: <span class="title function_">btoa</span>(process.<span class="property">env</span>.<span class="property">VUE_APP_AZURE_DUMMY_USER</span>), <span class="comment">// ナチュラルキーっぽくする</span></span><br><span class="line">      <span class="attr">ver</span>: <span class="string">&#x27;2.0&#x27;</span></span><br><span class="line">    &#125;)</span><br><span class="line">      .<span class="title function_">setIssuer</span>(<span class="string">`<span class="subst">$&#123;process.env.VUE_APP_AZURE_ISSUER&#125;</span>/v2.0`</span>)</span><br><span class="line">      .<span class="title function_">setAudience</span>(process.<span class="property">env</span>.<span class="property">VUE_APP_AZURE_APP_ID</span>)</span><br><span class="line">      .<span class="title function_">setIssuedAt</span>()</span><br><span class="line">      .<span class="title function_">setExpirationTime</span>(<span class="string">&#x27;1h&#x27;</span>)</span><br><span class="line">      .<span class="title function_">setNotBefore</span>(<span class="title class_">Date</span>.<span class="title function_">now</span>() / <span class="number">1000</span>)</span><br><span class="line">      .<span class="title function_">encode</span>()</span><br><span class="line">    idToken = jwt</span><br><span class="line">  &#125; <span class="keyword">else</span> &#123; <span class="comment">// 本番モード</span></span><br><span class="line">    <span class="keyword">if</span> (_auth.<span class="title function_">getAllAccounts</span>().<span class="property">length</span> &gt; <span class="number">0</span>) &#123;</span><br><span class="line">      _auth.<span class="title function_">setActiveAccount</span>(_auth.<span class="title function_">getAllAccounts</span>()[<span class="number">0</span>])</span><br><span class="line">      <span class="keyword">const</span> result = <span class="keyword">await</span> _auth.<span class="title function_">acquireTokenSilent</span>(&#123;</span><br><span class="line">        <span class="attr">scopes</span>: [<span class="string">`<span class="subst">$&#123;process.env.VUE_APP_AZURE_APP_ID&#125;</span>/.default`</span>],</span><br><span class="line">        <span class="attr">redirectUri</span>: config.<span class="property">auth</span>.<span class="property">redirectUri</span></span><br><span class="line">      &#125;)</span><br><span class="line">      idToken = result.<span class="property">idToken</span></span><br><span class="line">      <span class="keyword">return</span> idToken</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">      <span class="keyword">return</span> _auth.<span class="title function_">acquireTokenRedirect</span>(&#123;</span><br><span class="line">        <span class="attr">redirectStartPage</span>: location.<span class="property">href</span>,</span><br><span class="line">        <span class="attr">scopes</span>: [<span class="string">`<span class="subst">$&#123;process.env.VUE_APP_AZURE_APP_ID&#125;</span>/.default`</span>],</span><br><span class="line">        <span class="attr">redirectUri</span>: config.<span class="property">auth</span>.<span class="property">redirectUri</span></span><br><span class="line">      &#125;)</span><br><span class="line">    &#125;</span><br><span class="line">  &#125;</span><br><span class="line">&#125;,</span><br><span class="line"><span class="keyword">async</span> <span class="title function_">logout</span>(<span class="params"></span>) &#123;</span><br><span class="line">  <span class="keyword">if</span> (!process.<span class="property">env</span>.<span class="property">AZURE_DUMMY_USER</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> _auth.<span class="title function_">logoutRedirect</span>(&#123;</span><br><span class="line">      <span class="attr">postLogoutRedirectUri</span>: <span class="string">`<span class="subst">$&#123;location.protocol&#125;</span>//<span class="subst">$&#123;location.host&#125;</span>/`</span></span><br><span class="line">    &#125;)</span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div>

<p>これで、AzureADがあるふりをしてそれっぽいIDトークンを作って返すコードができました。</p>
<p>サーバー側ではリクエストを受けるときにこのトークンを受けることになります。サーバー側も環境変数で少し動作をコントロールして、テストモードの時には署名の検証は行わない必要がありますが、expiration time(exp)、not before(nbf)、aud、issといったクレームを使った検証は可能です。</p>
<h2 id="まとめ">まとめ</h2><p>ログインが必要なサービスで、開発時にログイン回りをどう処理すればいいのか、というのはいつも悩むポイントです。いろんなログイン方式が使えるサーバーであればID&#x2F;パスワードでログインする機構を別に作ったり、本番同等の認証サーバーを立てて、テストユーザーを入れるなどもあるでしょう。ですが、外部システムへの依存があると結合テストやCIがやりにくくなったりもしますし、処理時間も伸びてしまいます。あと、せっかくMSAL.jsを使えば認証の組み込みが簡単なのに、認証回り以外にたくさんのif文が入るのもうれしくありません。</p>
<p>今回はテスト用にAzureADのログインをバイパスしダミーのJWTを作るという方向で実装しました。比較的影響範囲をログイン回りに閉じ込めつつ実装できたんじゃないかな、と思います。</p>
]]></content>
    <summary type="html">MSAL.jsはとても便利なライブラリです。コールバックを受けるバックエンドサーバーの用意も不要で、フロントエンドだけで認証が完結します。ですが、開発時にAzureADがない場合もありますし、開発者全員が開発で使うAzureADにユーザー登録されていないかもしれません。</summary>
    <category term="認証認可" scheme="https://future-architect.github.io/categories/%E8%AA%8D%E8%A8%BC%E8%AA%8D%E5%8F%AF/"/>
    <category term="Azure" scheme="https://future-architect.github.io/tags/Azure/"/>
    <category term="EntraID" scheme="https://future-architect.github.io/tags/EntraID/"/>
    <category term="MSAL.js" scheme="https://future-architect.github.io/tags/MSAL-js/"/>
  </entry>
  <entry>
    <title>Auth0全ユーザー数取得コマンドをPowerShellのInvokeコマンドで行う</title>
    <link href="https://future-architect.github.io/articles/20221130a/"/>
    <id>https://future-architect.github.io/articles/20221130a/</id>
    <published>2022-11-29T15:00:00.000Z</published>
    <updated>2022-11-29T15:00:00.000Z</updated>
    <author><name>ダワージャルガルオチラル</name></author>
    <content type="html"><![CDATA[<h2 id="はじめに">はじめに</h2><p>Auth0のドキュメントに記載されているAPI操作は、curlコマンドで記載されています。</p>
<p>一方で、PowerShell標準の <code>Invoke-webRequest</code>や<code>Invoke-RestMethod</code> を用いて操作するといった日本語情報が少ないと思ったため、GETとPOSTリクエストの方法をまとめました。</p>
<p>ついでに、Auth0にいる全ユーザー数を取得する方法も共有します。</p>
<h2 id="Windowsのcurl事情">Windowsのcurl事情</h2><p>CLIから通信を行える便利コマンド <code>curl</code> は元々UNIX系のコマンドで、もともとWindowsにはインストールされていませんでした。</p>
<p>こちらの記事によると、2018年のWindows 10 Ver.1803からCurl.exeがWindowsにデフォルトで使えるようになったそうです。そこからは、コマンドプロンプトなら、<code>curl</code>、PowerShellの場合<code>curl.exe</code>と打てばcurlが使えます。</p>
<p>ここで大事なことですが、2018年までcurlが使えなかった時代の名残なのか、 <strong>PowerShellの場合、<code>curl</code> と打つとWindows用の<code>curl</code>であった<code>Invoke-WebRequest</code>が実行されてしまいます</strong>（curl.exeだとcurlが動くが、curlにはinvokeコマンドのエイリアスが貼ってある）。普段Windows環境を触らない人にとって、高度な罠ですね。</p>
<div class="code-block"><figure class="highlight plaintext"><input type="checkbox" id="code-wrap-11wg47j-1" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-11wg47j-1" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">PS C:\Users\xxxx&gt; curl</span><br><span class="line"></span><br><span class="line">コマンド パイプライン位置 1 のコマンドレット Invoke-WebRequest</span><br><span class="line">次のパラメーターに値を指定してください:</span><br><span class="line">Uri:</span><br></pre></td></tr></table></figure></div>

<p>そのため、筆者のようにPowerShellでは 一般にイメージする <code>curl</code> がないものだと認識し、<code>Invoke-webRequest</code> や <code>Invoke-RestMethod</code> を使う必要があると勘違いする人も少なくないと思います。今回の記事は一連のAuth0のドキュメントにあったcurlコマンドをInvoke-RestMethodに置換して実行する流れを、一晩かけて勢いでまとめた記事です。</p>
<p>すべてを書き終えた後、先輩社員に<code>curl.exe</code>すればcurl出来るよと言われ悲しくなりましたが、2023年10月、2027年1月までサポートを受けているWindows Server 2012、2016にはcurlがないと思われるので、そういった環境を扱う方には有用だと思います。ちなみに、Windows Server 2019には <code>curl.exe</code> がありましたので、素直にそちらで操作すると良いでしょう。</p>
<p>注意ですが、この記事に記載しているcurlコマンドをコマンドプロンプト上で動かす場合は、<code>\</code>のエスケープと、改行を消す必要があります（記事上では読みやすさのために改行を入れています）。</p>
<h2 id="結論から話すと">結論から話すと</h2><p>以下のコマンドで動きます。</p>
<ol>
<li>token取得（postリクエスト）</li>
</ol>
  <div class="code-block"><figure class="highlight powershell"><input type="checkbox" id="code-wrap-11wg47j-2" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-11wg47j-2" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="variable">$client_id</span> =  <span class="string">&quot;xxx&quot;</span></span><br><span class="line"><span class="variable">$client_secret</span> =  <span class="string">&quot;xxx&quot;</span></span><br><span class="line"><span class="variable">$api</span> =  <span class="string">&quot;https://xxx/api/v2/&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="variable">$body</span> = <span class="selector-tag">@</span>&#123;</span><br><span class="line">    client_id = <span class="string">&quot;<span class="variable">$client_id</span>&quot;</span></span><br><span class="line">    client_secret = <span class="string">&quot;<span class="variable">$client_secret</span>&quot;</span></span><br><span class="line">    audience = <span class="string">&quot;<span class="variable">$api</span>&quot;</span></span><br><span class="line">    grant_type = <span class="string">&quot;client_credentials&quot;</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="built_in">Invoke-RestMethod</span> <span class="literal">-Method</span> Post <span class="literal">-Uri</span> <span class="string">&quot;https://xxx/oauth/token&quot;</span> <span class="literal">-ContentType</span> <span class="string">&#x27;application/json&#x27;</span> <span class="literal">-Body</span> (<span class="variable">$body</span>|<span class="built_in">ConvertTo-Json</span>) <span class="literal">-OutFile</span> output.txt</span><br><span class="line"><span class="built_in">cat</span> output.txt</span><br></pre></td></tr></table></figure></div>

<ol start="2">
<li>output.txtからtokenをコピーして2のコマンドを打つ</li>
<li>全ユーザー数取得コマンド(getリクエスト)</li>
</ol>
  <div class="code-block"><figure class="highlight powershell"><input type="checkbox" id="code-wrap-11wg47j-3" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-11wg47j-3" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="variable">$token</span> = <span class="string">&quot;copyAndPasteHere&quot;</span></span><br><span class="line"><span class="built_in">Invoke-RestMethod</span> <span class="literal">-Method</span> Get <span class="literal">-Uri</span> <span class="string">&quot;https://xxx/api/v2/users?per_page=0&amp;include_totals=true&quot;</span> <span class="literal">-Headers</span> <span class="selector-tag">@</span>&#123;Authorization=<span class="string">&quot;Bearer <span class="variable">$token</span>&quot;</span>&#125;</span><br></pre></td></tr></table></figure></div>

<ol start="4">
<li>output.txt が不要になれば削除します</li>
</ol>
  <figure class="highlight powershell"><table><tr><td class="code"><pre><span class="line"><span class="built_in">rm</span> .\output.txt</span><br></pre></td></tr></table></figure>

<h2 id="操作の流れ">操作の流れ</h2><p>Auth0にいる総ユーザー数を取得を <code>Invoke-RestMethod</code> で記載する方法を共有します。</p>
<p>基本的には以下の2つのコマンドを<code>Invoke-RestMethod</code> で代替します。</p>
<ol>
<li>APIを利用するtokenを取得する（POSTリクエスト）</li>
<li>総ユーザー数取得APIを打つ（GETリクエスト）</li>
</ol>
<h3 id="1-APIを利用するtokenを取得する（POSTリクエスト）">1. APIを利用するtokenを取得する（POSTリクエスト）</h3><p>ユーザー数取得に使う <strong>Auth0 User Management API</strong> を利用するためのtokenをまずは取得します。</p>
<p>User Management APIの利用権限のあるAPIのtoken取得コマンドが、<strong>API設定のTestタブに</strong>以下の画像のように書いてあるので参照します。tokenを取得する<code>cURLコマンド</code>と、すごく親切にバックエンドでよく用いる言語での取得方法まで記載しているので参考になります。</p>
<img fetchpriority="high" src="/images/2022/20221130a/0.png" alt="" width="1200" height="706">

<div class="code-block"><figure class="highlight plaintext"><input type="checkbox" id="code-wrap-11wg47j-4" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-11wg47j-4" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">curl --request POST \</span><br><span class="line">  --url https://$domain/oauth/token \</span><br><span class="line">  --header &#x27;content-type: application/json&#x27; \</span><br><span class="line">  --data &#x27;&#123;&quot;client_id&quot;:&quot;alphanumericWithCapita1Letter&quot;,&quot;client_secret&quot;:&quot;alphanumericWithCapita1LetterChottoNaga1&quot;,&quot;audience&quot;:&quot;https://$domain/api/v2/&quot;,&quot;grant_type&quot;:&quot;client_credentials&quot;&#125;&#x27;</span><br></pre></td></tr></table></figure></div>

<h4 id="観察">観察</h4><p>まず元のCURLが何やってるか見ます。</p>
<ul>
<li><strong>POST</strong>リクエスト</li>
<li>content typeが<strong>application&#x2F;json</strong>形式</li>
<li>dataに<strong>json文字列でclient認証情報を渡している</strong></li>
</ul>
<p><strong>data</strong>とありますが <strong>HTTPリクエストではbody</strong> とも呼びます。ここまでで、 <strong><code>Invoke-RestMethod</code>でやることは「JSONをPOSTするリクエストを作れば良い</strong>」ということが分かります。</p>
<h4 id="公式ドキュメント見る">公式ドキュメント見る</h4><p>2022年11月時点ではpowershell-7.3が最新のようで、公式ドキュメントはこれです。</p>
<figure class="highlight powershell"><table><tr><td class="code"><pre><span class="line"><span class="built_in">Invoke-RestMethod</span></span><br><span class="line">      [-<span class="type">Method</span> &lt;<span class="type">WebRequestMethod</span>&gt;]</span><br><span class="line">      [-<span class="type">FollowRelLink</span>]</span><br><span class="line">      [-<span class="type">MaximumFollowRelLink</span> &lt;<span class="built_in">Int</span><span class="type">32</span>&gt;]</span><br><span class="line">      [-<span class="type">ResponseHeadersVariable</span> &lt;<span class="built_in">String</span>&gt;]</span><br><span class="line">      [-<span class="type">StatusCodeVariable</span> &lt;<span class="built_in">String</span>&gt;]</span><br><span class="line">      [-<span class="type">UseBasicParsing</span>]</span><br><span class="line">      [-<span class="type">Uri</span>] &lt;Uri&gt;</span><br><span class="line">      [-<span class="type">HttpVersion</span> &lt;<span class="type">Version</span>&gt;]</span><br><span class="line">...</span><br></pre></td></tr></table></figure>

<p>中々難しそうですが、コレを見ると、 <strong><code>Invoke-RestMethod</code>で各オプションを付ければ良い</strong> ことが推測できます。</p>
<p><strong>CURLで指定したオプションは以下のようにマッピング出来そう</strong>ですね。</p>
<ul>
<li>–requestは-Method<ul>
<li><code>-Method Post</code></li>
</ul>
</li>
<li>–urlは-Uri<ul>
<li><code>-Uri https://$domain/oauth/token</code></li>
</ul>
</li>
<li>–headerはHeadersとContentTypeが両方ありますね、ContentTypeだけ指定するので-ContentTypeのみ使います（Headersにcontent-typeと入れたらエラーになってました）<ul>
<li><code>-ContentType application/json</code></li>
</ul>
</li>
<li>–dataは-body<ul>
<li>後述しますがいい感じに書かないとNGでした</li>
</ul>
</li>
</ul>
<p>これで、<strong>bodyに当たる部分以外は良い感じにマッピング出来ました。</strong></p>
<p>続いてはbodyの記載方法を見ます。</p>
<img src="/images/2022/20221130a/image.png" alt="" width="700" height="1083" loading="lazy">

<p>ぱっと見は理解することが難しいですよね。オブジェクトで渡せば良いのかな？ とわかります。</p>
<p>公式にPOSTの例があるので参考にできます。</p>
<div class="code-block"><figure class="highlight powershell"><input type="checkbox" id="code-wrap-11wg47j-5" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-11wg47j-5" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="variable">$Cred</span> = <span class="built_in">Get-Credential</span></span><br><span class="line"><span class="variable">$Url</span> = <span class="string">&quot;https://server.contoso.com:8089/services/search/jobs/export&quot;</span></span><br><span class="line"><span class="variable">$Body</span> = <span class="selector-tag">@</span>&#123;</span><br><span class="line">    search = <span class="string">&quot;search index=_internal | reverse | table index,host,source,sourcetype,_raw&quot;</span></span><br><span class="line">    output_mode = <span class="string">&quot;csv&quot;</span></span><br><span class="line">    earliest_time = <span class="string">&quot;-2d@d&quot;</span></span><br><span class="line">    latest_time = <span class="string">&quot;-1d@d&quot;</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="built_in">Invoke-RestMethod</span> <span class="literal">-Method</span> <span class="string">&#x27;Post&#x27;</span> <span class="literal">-Uri</span> <span class="variable">$url</span> <span class="literal">-Credential</span> <span class="variable">$Cred</span> <span class="literal">-Body</span> <span class="variable">$body</span> <span class="literal">-OutFile</span> output.csv</span><br></pre></td></tr></table></figure></div>

<p>どうやらシェル内でオブジェクトを作れば良さそうだとわかります。この例を参考に以下のように動かすと <strong>エラーになります</strong>。</p>
<div class="code-block"><figure class="highlight powershell"><input type="checkbox" id="code-wrap-11wg47j-6" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-11wg47j-6" title="コードの折り返しを切り替える"></label><figcaption><span>エラーになった実行結果</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="variable">$body</span> = <span class="selector-tag">@</span>&#123;</span><br><span class="line">    client_id = <span class="string">&quot;alphanumericWithCapita1Letter&quot;</span></span><br><span class="line">	client_secret = <span class="string">&quot;alphanumericWithCapita1LetterChottoNaga1&quot;</span></span><br><span class="line">	audience = <span class="string">&quot;https://<span class="variable">$domain</span>/api/v2/&quot;</span></span><br><span class="line">	grant_type = <span class="string">&quot;client_credentials&quot;</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="built_in">Invoke-RestMethod</span> <span class="literal">-Method</span> Post <span class="literal">-Uri</span> <span class="string">&quot;https://<span class="variable">$domain</span>/oauth/token&quot;</span> <span class="literal">-ContentType</span> <span class="string">&#x27;application/json&#x27;</span> <span class="literal">-Body</span> <span class="variable">$body</span></span><br></pre></td></tr></table></figure></div>

<figure class="highlight powershell"><figcaption><span>実行結果</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="built_in">Invoke-RestMethod</span> : invalid json</span><br></pre></td></tr></table></figure>

<p>理由ですが、bodyにjson渡す渡す詐欺（コンテンツタイプでJSON渡すと宣言してるがJSONを渡していない状態）をしてるようです。よしなにやってくれると少し期待しましたが、ダメなようです。<br>（※もし、何かしらの手法があれば教えてください）</p>
<h4 id="対応方法">対応方法</h4><p><code>auth0 invoke rest method post body json powershell</code> といったキーワードで探すと、こちらの記事に記載している通り、 <code>ConvertTo-Json</code><strong>コマンドを用いbodyのオブジェクトをJSONに変換</strong>すれば良いということがわかります（<code>-Body $body</code> ➔　<code>-Body ($body|ConvertTo-Json)</code>）。</p>
<p>※公式ドキュメントの関連記事の箇所にも <code>ConvertTo-Json</code> の記載がありますが、本文にも記載があると助かる人もいるかなと思い、公式ドキュメントにフィードバックは出しておきました。これが採用されると嬉しいなと思います。</p>
<p>結果として、以下のコマンドで動きます。</p>
<div class="code-block"><figure class="highlight powershell"><input type="checkbox" id="code-wrap-11wg47j-7" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-11wg47j-7" title="コードの折り返しを切り替える"></label><figcaption><span>成功例</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="variable">$body</span> = <span class="selector-tag">@</span>&#123;</span><br><span class="line">    client_id = <span class="string">&quot;alphanumericWithCapita1Letter&quot;</span></span><br><span class="line">	client_secret = <span class="string">&quot;alphanumericWithCapita1LetterChottoNaga1&quot;</span></span><br><span class="line">	audience = <span class="string">&quot;https://<span class="variable">$domain</span>/api/v2/&quot;</span></span><br><span class="line">	grant_type = <span class="string">&quot;client_credentials&quot;</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="built_in">Invoke-RestMethod</span> <span class="literal">-Method</span> Post <span class="literal">-Uri</span> <span class="string">&quot;https://<span class="variable">$domain</span>/oauth/token&quot;</span> <span class="literal">-ContentType</span> <span class="string">&#x27;application/json&#x27;</span> <span class="literal">-Body</span> (<span class="variable">$body</span>|<span class="built_in">ConvertTo-Json</span>)</span><br></pre></td></tr></table></figure></div>

<p>しかし、少し斜め上な結果になります。</p>
<img src="/images/2022/20221130a/1.png" alt="1.png" width="1200" height="181" loading="lazy">

<h4 id="出力結果最後まで出ない問題">出力結果最後まで出ない問題</h4><p>PowerShellの仕様か、Invoke-RestMethodの仕様なのか、<strong>出力が最後まで出てくれずトークンが分からない問題</strong> が発生しました。</p>
<p>解決策として、公式の例を真似て<strong>ファイルに出力して表示</strong>することにします（愚直に<code>output.txt</code>に出して<code>cat output.txt</code>します）。シェルに詳しい人だったら良い感じにCLIの出力出来たかもしれないですが、詳しい方は教えてください。</p>
<p>そのため、以下のコマンドを付けます。</p>
<figure class="highlight powershell"><table><tr><td class="code"><pre><span class="line">... <span class="literal">-OutFile</span> output.txt</span><br><span class="line"><span class="built_in">cat</span> output.txt</span><br></pre></td></tr></table></figure>

<h3 id="総ユーザー数取得APIを打つ">総ユーザー数取得APIを打つ</h3><p>最初に、Auth0全ユーザー数の取得コマンドを探すため、公式で用意されているAuth0 User Management APIのドキュメントを見ます。</p>
<p>そうすると、<code>Users</code>　➔　<code>List or Search Users</code>の箇所のパラメータを眺めてると <strong>小さく取得できる旨が書いて</strong> あります。APIの概要にはページング番号を指定しながらのユーザー取得しかできないかのように書いてあるが、よくよくパラメータを見ると取得できることがわかります。</p>
<p><strong>API概要</strong>:1ページに取得されるユーザー数を指定してユーザーリストを取得できるんやでと記載されています。</p>
<div class="code-block"><figure class="highlight txt"><input type="checkbox" id="code-wrap-11wg47j-8" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-11wg47j-8" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">Retrieve details of users. It is possible to:</span><br><span class="line"></span><br><span class="line">- Specify a search criteria for users</span><br><span class="line">- Sort the users to be returned</span><br><span class="line">- Select the fields to be returned</span><br><span class="line">- Specify the number of users to retrieve per page and the page index</span><br></pre></td></tr></table></figure></div>

<p><strong>パラメータ</strong>:include_totalsをオンにすると<strong>APIのレスポンスにトータルを含められる</strong>と書いています。</p>
<div class="code-block"><figure class="highlight text"><input type="checkbox" id="code-wrap-11wg47j-9" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-11wg47j-9" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">include_totals</span><br><span class="line">Return results inside an object that contains the total result count (true) or as a direct array of results (false, default).</span><br></pre></td></tr></table></figure></div>

<p>しかし、この説明文だと、表示するページの合計なのか、全体なのか曖昧ですよね。</p>
<img src="/images/2022/20221130a/image_2.png" alt="image.png" width="612" height="200" loading="lazy">

<p>また、概要にある通りユーザーのリストが取得できてしまうが、合計人数だけ知りたいので<strong>ユーザー情報をなくすオプションを探します</strong>。※全パラメータは任意。</p>
<div class="scroll"><table>
<thead>
<tr>
<th>パラメータ</th>
<th>説明</th>
</tr>
</thead>
<tbody><tr>
<td>page</td>
<td>返却するページ番号（0インデックス）</td>
</tr>
<tr>
<td>per_page</td>
<td>1ページに含むユーザー数、空の場合全件返却</td>
</tr>
<tr>
<td>include_totals</td>
<td>レスポンスに合計人数を入れる</td>
</tr>
<tr>
<td>sort</td>
<td>ソート項目・順を決める</td>
</tr>
<tr>
<td>connection</td>
<td>コネクションフィルター（よく分からず）</td>
</tr>
<tr>
<td>fields</td>
<td>表示&#x2F;非表示する項目を決める。空の場合全項目返却</td>
</tr>
<tr>
<td>include_fields</td>
<td>fieldsで指定した項目を表示させるか非表示にするか決める</td>
</tr>
<tr>
<td>q</td>
<td>検索クエリ、形式はLucene query string syntaxらしい</td>
</tr>
<tr>
<td>search_engine</td>
<td>サーチエンジンを決める、詳細はなかったため謎</td>
</tr>
</tbody></table></div>
<p><code>per_page</code>に着目すると<code>include_totals</code>だけ指定して<code>per_page</code>を<strong>空にした場合全ユーザー情報が取得できてしまう</strong>ようです。そしてユーザー取得フラグのようなものはなく、 <strong><code>per_page</code>をいじるしかなさそう</strong> なので、一旦これを0にしてAPIを実行することにします。</p>
<p>token取得時と同様に、まず成功するcurlコマンドを共有します。401認証失敗エラーにならないようにtokenをつけます。</p>
<div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-11wg47j-10" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-11wg47j-10" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">curl --request GET \</span><br><span class="line">  --url <span class="string">&quot;https://<span class="variable">$domain</span>/api/v2/users?per_page=0&amp;include_totals=true&quot;</span> \</span><br><span class="line">  --header <span class="string">&#x27;authorization: Bearer $token&#x27;</span></span><br></pre></td></tr></table></figure></div>

<h3 id="観察・マッピング">観察・マッピング</h3><p>クエリパラメータはGETなのでシンプルですね。token渡したGETリクエストするだけです。公式ドキュメントのリンクはこちらです。</p>
<ul>
<li>GETリクエストをしている<ul>
<li><code>--request GET</code>が<code>--Method Get</code>になる</li>
</ul>
</li>
<li>URLにPOSTと違いクエリパラメータがある<ul>
<li><code>--url</code>が<code>-Uri</code>になる</li>
<li><code>-Uri https://$domain/api/v2/users?per_page=0&amp;include_totals=true</code></li>
<li>URLにパラメータを入れることをクエリパラメータと言う</li>
</ul>
</li>
<li>token認証情報を渡している<ul>
<li><code>--header</code>が<code>-Headers</code>になる</li>
<li><code>-Headers @&#123;Authorization=&quot;Bearer $token&quot;&#125;</code></li>
<li>cURLと違い<code>Invoke-RestMethod</code>特有のオブジェクト形式で書かないといけないので@{xxx}の形式となる</li>
<li>Authenticationオプションなどでも指定可能だったかもしれない（未検証）</li>
</ul>
</li>
</ul>
<p>以上からInvokeコマンドに書き換えます。</p>
<div class="code-block"><figure class="highlight powershell"><input type="checkbox" id="code-wrap-11wg47j-11" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-11wg47j-11" title="コードの折り返しを切り替える"></label><figcaption><span>実行例</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="built_in">Invoke-RestMethod</span> <span class="literal">-Method</span> Get <span class="literal">-Uri</span> <span class="string">&quot;https://<span class="variable">$domain</span>/api/v2/users?per_page=0&amp;include_totals=true&quot;</span> <span class="literal">-Headers</span> <span class="selector-tag">@</span>&#123;Authorization=<span class="string">&quot;Bearer <span class="variable">$token</span>&quot;</span>&#125;</span><br></pre></td></tr></table></figure></div>

<figure class="highlight powershell"><figcaption><span>実行結果</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="built_in">start</span>  : <span class="number">0</span></span><br><span class="line">limit  : <span class="number">0</span></span><br><span class="line">length : <span class="number">0</span></span><br><span class="line">users  : &#123;&#125;</span><br><span class="line">total  : xxx</span><br></pre></td></tr></table></figure>

<p>無事totalの数字が取得できました！</p>
<h2 id="さいごに">さいごに</h2><p>curlコマンドの代替として、PowerShell標準の <code>Invoke-webRequest</code>だったり<code>Invoke-RestMethod</code> を用いてAuth0のAPIを操作する例をまとめました。</p>
<p>IT初心者がIT課題をどう解決していけば良いのか何となく分かるような文章を書けたら良いなと最近考えているため、ハマった部分や調査の流れもなるべく残すように記載しました。ググっても情報が見つかりにくかったことを記事にして誰かを助ける備忘録にもなってたら良いなと思います。</p>
<p>この記事が良いなと思ったら感想下さると励みになります。Twitterなどでコメントいただけると幸いです。</p>
]]></content>
    <summary type="html">Auth0全ユーザー数取得コマンドをPowerShellのInvokeコマンドで行います。</summary>
    <category term="認証認可" scheme="https://future-architect.github.io/categories/%E8%AA%8D%E8%A8%BC%E8%AA%8D%E5%8F%AF/"/>
    <category term="Auth0" scheme="https://future-architect.github.io/tags/Auth0/"/>
    <category term="PowerShell" scheme="https://future-architect.github.io/tags/PowerShell/"/>
    <category term="Windows" scheme="https://future-architect.github.io/tags/Windows/"/>
    <category term="curl" scheme="https://future-architect.github.io/tags/curl/"/>
  </entry>
  <entry>
    <title>AzureAD＋MSAL for Goでバッチコマンドの認証</title>
    <link href="https://future-architect.github.io/articles/20221122a/"/>
    <id>https://future-architect.github.io/articles/20221122a/</id>
    <published>2022-11-21T15:00:00.000Z</published>
    <updated>2022-11-21T15:00:00.000Z</updated>
    <author><name>澁川喜規</name></author>
    <content type="html"><![CDATA[<p>前回の記事ではMSAL.jsを使い、シングルページアプリケーションの認証を試してみました。</p>
<p>業務システムで扱う認証にはいろいろな種類がありますが、だいたい以下のどれかに該当するケースが多いと思います。</p>
<ul>
<li>Webサービス・モバイルアプリ: 一般ユーザーでログイン</li>
<li>デスクトップで動かすバッチコマンド: 一般ユーザーでログイン</li>
<li>デスクトップやサーバーで動かすバッチコマンド: 無人運用</li>
</ul>
<p>Webサービスのうち、SPAは前回のエントリーで説明しました。Webサービスの認証は前回説明しました。今時の動的ページはSPAが主流と考えれば旧来のOpenID Connect（コールバックをウェブサーバーで受けてトークン発行はサーバーで行う）は説明不要でしょう。モバイルアプリについては使うフレームワークによっても変わるので割愛します。</p>
<p>本稿では、それ以外のケースとして、バッチコマンドの認証について扱います。今度はウェブ以外の認証ということで、MSAL for Goを使って認証します。上にあげたように、一般ユーザーでログインするケースと、無人運用の2つのケースを取り上げます。</p>
<h2 id="一般ユーザーの認証">一般ユーザーの認証</h2><p>一般ユーザーは、WindowsとかOffice 365とかにログインする、いわゆる普通のユーザーです。この権限でトークンをとってAPIを実行すると、そのユーザーが操作したことになります。コマンドを動かした人の名前がログが残るということです。一般ユーザーの場合は、コマンドはまず、ユーザーに「お前誰よ」と聞く必要があります。</p>
<p>コマンドが自前でユーザーIDとパスワードの入力欄を出して入力させ、それを認証で使うフロー（Resource Owner Password Credentials Flow）は以前はありましたが、OAuth 2.1で無くなることが確定しています。ブラウザを表示してAzureAD認証をユーザーに行ってもらい、その結果のコードを使ってトークンを取得する方法がOAuth 2.1時代に唯一現存する方法です。そのため、通信方式としては、前回のSPAモードと同じく、Authorization Code Flowとなります。この方式はSPAと同様にパブリッククライアント用のモードなのでバッチコマンドを悪意のあるユーザーに奪取されて解析されたとしても直接それがセキュリティホールにはなりません。</p>
<p>まずは、AzureADの管理画面でアプリケーションを登録します（前回同様）。前回同様、テナントIDとクライアントIDはメモしておきます。</p>
<p>その後、認証のセクションで認証方式を追加しますが、今回はモバイルアプリケーションとデスクトップアプリケーションを選択し、カスタムのコールバックのアドレスで、ローカルホストのパスを指定します。ポートも指定する必要があります。また、<code>/callback</code>などのパスは不要です（後述）。</p>
<img fetchpriority="high" src="/images/2022/20221122a/スクリーンショット_2022-11-10_16.45.58.png" alt="" width="1200" height="570">

<p>Go版のMSALは以下のようにしてインポートします。</p>
<div class="code-block"><figure class="highlight bash"><input type="checkbox" id="code-wrap-164gldd-1" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-164gldd-1" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">go get github.com/AzureAD/microsoft-authentication-library-for-go</span><br></pre></td></tr></table></figure></div>

<p>なお、追加でいくつかimportしないとエラーが出ます。不思議な構成。</p>
<div class="code-block"><figure class="highlight bash"><input type="checkbox" id="code-wrap-164gldd-2" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-164gldd-2" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">go get github.com/AzureAD/microsoft-authentication-library-for-go/apps/internal/oauth/ops/accesstokens@v0.7.0</span><br><span class="line">go get github.com/AzureAD/microsoft-authentication-library-for-go/apps/errors@v0.7.0</span><br><span class="line">go get github.com/AzureAD/microsoft-authentication-library-for-go/apps/public@v0.7.0</span><br></pre></td></tr></table></figure></div>

<p>モバイルアプリとかのパブリッククライアントは<code>.../apps/public</code>パッケージにあります。前回のエントリーでも紹介したパブリッククライアント用のパッケージです。これを使ったバイナリはリバースエンジニアリングされても、不正ログインされる材料は提供しません。</p>
<p>このライブラリを使ったコードは以下の通りで、JavaScript版とほぼ同じAPIで似たように書けます。</p>
<div class="code-block"><figure class="highlight go"><input type="checkbox" id="code-wrap-164gldd-3" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-164gldd-3" title="コードの折り返しを切り替える"></label><figcaption><span>main.go</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;context&quot;</span></span><br><span class="line">    <span class="string">&quot;log&quot;</span></span><br><span class="line"></span><br><span class="line">    <span class="string">&quot;github.com/AzureAD/microsoft-authentication-library-for-go/apps/public&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">	pc, err := public.New(<span class="string">&quot;&#123;クライアントID&#125;&quot;</span>, public.WithAuthority(<span class="string">&quot;https://login.microsoftonline.com/&#123;テナントID&#125;&quot;</span>))</span><br><span class="line">	<span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">		log.Fatal(err)</span><br><span class="line">	&#125;</span><br><span class="line">	result, err := pc.AcquireTokenInteractive(context.Background(), []<span class="type">string</span>&#123;<span class="string">&quot;User.Read&quot;</span>&#125;, public.WithRedirectURI(<span class="string">&quot;&#123;コールバックURL&#125;&quot;</span>))</span><br><span class="line">	<span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">		log.Fatal(err)</span><br><span class="line">	&#125;</span><br><span class="line">	log.Println(result.AccessToken)</span><br><span class="line">&#125;</span><br><span class="line"></span><br></pre></td></tr></table></figure></div>

<p>これだけで実現できました。</p>
<h3 id="一般ユーザー方式の蛇足な説明">一般ユーザー方式の蛇足な説明</h3><p>Go版のコードをみると、コールバックURLをパースして、ポート番号を取り出して自分でウェブサーバーを起動し、ブラウザからのリダイレクトを受けられるようにしています。このサーバーはコールバックのパスの部分を認識してくれないため、AzureADの登録では<code>http://localhost:5173</code>のような形式にしないと「コールバックアドレスが登録と違う」というエラーになってしまいます。また、コールバックアドレスを設定しないと、ランダムなポート番号で起動します。ただ、ポート番号が一致しないと失敗となるので、何かしらのポートを登録しないといけないはずです。</p>
<p>認証方式でお手軽だったSPAを選ぶとよさそうですが、これは「cross-origin requestsじゃないとダメ」というエラーが出ます。また、一般のウェブを選ぶと「client_assertion’ or ‘client_secret」が必要というエラーが出るので、今回選んだ「モバイルアプリケーションとデスクトップアプリケーション」一択です。</p>
<p>また、モバイルアプリケーション云々では、独自のスキーマのコールバックURLを自動で作ってくれていました。MSAL用とあるので使えそうですが、これはin app browserなど、特定のスキーマの通信を横取りできる環境ようになっています。今回は一般のブラウザを使っているのでこの方式は使えません。</p>
<h2 id="無人運用の認証">無人運用の認証</h2><p>バッチ処理などではログイン画面を出したりはできません。特定のユーザーのIDやパスワードを焼き込んで使い、退職にともなって停止して困った、みたいな話は昔から何度も聞きます。これは運用として間違っています。システムユーザー的なものを使って運用するのがベストです。しかし、前述のようにパスワードをツールが直接扱う認証は非推奨です。OAuth 2.1時代に使える方式としてはクライアントシークレットを使った認証方式になります。</p>
<p>まずはシークレットを生成します。「証明書とシークレット」を選択し、新しいクライアントシークレットを選択してシークレットを作ります。</p>
<img src="/images/2022/20221122a/スクリーンショット_2022-11-11_20.14.23.png" alt="スクリーンショット_2022-11-11_20.14.23.png" width="1200" height="497" loading="lazy">

<p>出来上がると、「値」と「シークレットID」が表示されますが、値の方が必要なものなので、コピーしておきます。</p>
<img src="/images/2022/20221122a/スクリーンショット_2022-11-11_21.34.44.png" alt="スクリーンショット_2022-11-11_21.34.44.png" width="1200" height="563" loading="lazy">

<p>これを組み込んだコードが以下の通りです。前回のエントリーや前述のパブリッククライアントのケースとは異なり、今回は<code>.../confidential</code>なパッケージを使っています。これはコンフィデンシャルクライアントで、攻撃者がバイナリにさわれない環境を想定しています。クライアントシークレットを奪取されてしまうとログインできてしまうのでこのバッチコマンドは（広く配布しない前提の）社内専用ツールだったり、バッチサーバーでのみ運用するケースでしか使ってはいけません。</p>
<div class="code-block"><figure class="highlight go"><input type="checkbox" id="code-wrap-164gldd-4" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-164gldd-4" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">&quot;context&quot;</span></span><br><span class="line">    <span class="string">&quot;log&quot;</span></span><br><span class="line"></span><br><span class="line">    <span class="string">&quot;github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">	s, err := confidential.NewCredFromSecret(<span class="string">&quot;&#123;クライアントシークレット&#125;&quot;</span>)</span><br><span class="line">	<span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">		log.Fatal(err)</span><br><span class="line">	&#125;</span><br><span class="line">	cc, err := confidential.New(<span class="string">&quot;&#123;クライアントID&#125;&quot;</span>, s,</span><br><span class="line">        confidential.WithAuthority(<span class="string">&quot;https://login.microsoftonline.com/&#123;テナントID&#125;&quot;</span>))</span><br><span class="line">	<span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">		log.Fatal(err)</span><br><span class="line">	&#125;</span><br><span class="line">	result, err := cc.AcquireTokenByCredential(context.Background(), []<span class="type">string</span>&#123;<span class="string">&quot;https://graph.microsoft.com/.default&quot;</span>&#125;)</span><br><span class="line">	<span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">		log.Fatal(err)</span><br><span class="line">	&#125;</span><br><span class="line">	log.Println(result.AccessToken)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div>

<p>要注意ポイントはスコープの指定です。「リソースのURL」と「権限（パス形式）」を組み合わせたURL形式で指定します。SharePointだと、<code>https://&#123;サイト名&#125;.sharepoint.com/&#123;権限&#125;</code>です。権限部分は<code>/.default</code>か、ここに書いてあるような<code>Sites.FullControl.All</code>のような文字列を使います。なぜパブリッククライアントの時と違う名前なのか・・・</p>
<h2 id="認証のキャッシュ">認証のキャッシュ</h2><p>バッチ処理を毎秒実行するとして、毎秒認証するのは無駄が多いでしょう。トークンが有効な間は同じトークンを使いまわしたいところです。MSAL for Goでは自分でキャッシュ機構を作ることが可能です。といっても、大体はファイルへの読み書きだと思うので、次のサンプルの通りに実装すればおしまいです。</p>
<p>https://github.com/AzureAD/microsoft-authentication-library-for-go/blob/dev/apps/tests/devapps/sample_cache_accessor.go</p>
<p>パブリッククライアントの場合は次のオプションを<code>New</code>に追加します。</p>
<figure class="highlight go"><table><tr><td class="code"><pre><span class="line">public.WithCache(&amp;TokenCache&#123;<span class="string">&quot;ファイル名&quot;</span>&#125;)</span><br></pre></td></tr></table></figure>

<p>コンフィデンシャルクライアントの場合は次のオプションを<code>New</code>に追加します。なぜ違う名前なのか・・・</p>
<figure class="highlight go"><table><tr><td class="code"><pre><span class="line">confidential.WithAccessor(cache)</span><br></pre></td></tr></table></figure>

<h2 id="まとめ">まとめ</h2><p>今回もバッチコマンドを想定してAzureADと認証するためのライブラリを使った認証を試してみました。</p>
<p>この手の検証は、アプリケーションのコード側だけではなく、接続先のAzureADの設定によっても接続が失敗する可能性があります。また、このあたりの設定はクリティカル度が高いため、アクセスできる人はなるべく少なくする運用がされることがほとんどです。特に受託開発で、お客さん側でAzureADの設定を管理している場合など、開発側では直接コンソールが触れずに、エスパーしながら試行錯誤しなければならない場面があります。お客さん側にも時間を取ってもらわないといけないし、自由な試行錯誤が難しかったりと、靴の裏から足の裏を掻くようなもどかしいことになります。<br>前回と今回のエントリーは、そのような場合にも対応できるように、AzureAD側の設定の依頼が投げやすいように、開発のストレスを下げたい、という思いで管理画面側の設定もなるべく具体的に書いています。</p>
<p>MSAL系のライブラリにはたくさんの実装がありますが、ウェブフロントエンドもGoも、APIはほぼ一緒でした。Javaとかみてみてもすぐにキャッチアップできそうです。簡単で安全な接続ができるため、接続先がAzureADであれば積極的にMSALシリーズを活用してみると良いと思いました。</p>
]]></content>
    <summary type="html">前回の記事ではMSAL.jsを使い、シングルページアプリケーションの認証を試してみました。業務システムで扱う認証にはいろいろな種類がありますが、だいたい以下のどれかに該当するケースが多いと思います。</summary>
    <category term="認証認可" scheme="https://future-architect.github.io/categories/%E8%AA%8D%E8%A8%BC%E8%AA%8D%E5%8F%AF/"/>
    <category term="AD" scheme="https://future-architect.github.io/tags/AD/"/>
    <category term="Azure" scheme="https://future-architect.github.io/tags/Azure/"/>
    <category term="EntraID" scheme="https://future-architect.github.io/tags/EntraID/"/>
    <category term="Go" scheme="https://future-architect.github.io/tags/Go/"/>
    <category term="MSAL.js" scheme="https://future-architect.github.io/tags/MSAL-js/"/>
  </entry>
  <entry>
    <title>MSAL.jsを使ってウェブフロントエンドだけでAzureAD認証する</title>
    <link href="https://future-architect.github.io/articles/20221118a/"/>
    <id>https://future-architect.github.io/articles/20221118a/</id>
    <published>2022-11-17T15:00:00.000Z</published>
    <updated>2022-11-17T15:00:00.000Z</updated>
    <author><name>澁川喜規</name></author>
    <content type="html"><![CDATA[<p><strong>11&#x2F;30更新</strong> スコープを<code>[&quot;User.Read&quot;]</code>としていましたが、<code>[&#39;&#123;クライアントID&#125;/.default&#39;]</code>にしないと署名がvalidなトークンにならないという罠がありましたので修正しています。</p>
<p>AzureADを使って認証している企業は多いと思います。このAzureADを使った場合にはMSAL.jsを使えば認証は楽だぞ、というのはAzureADのサイトには書かれているのですが、OpenID Connectのプロトコルの動きの理解と、ライブラリのAPIがどう対応づいているのかがわからずにちょっと試行錯誤したので、そのメモを残しておきます。</p>
<p>OAuth 2.1(現在策定中)ではImplicit Code Flowが非推奨になり、Authorization Code FlowにPKCEが追加されて、コード横取り攻撃への耐性が強化されて、モバイルアプリケーションや、ウェブフロントエンドなどのパブリッククライアント（ユーザー側で動作するため攻撃者が自由にいじれる）でも安全に認証できるようになります。OAuth 2.1自体はまだ作業中ではありますが、これはOAuth 2.0から少しずつ追加されたアップデートをまとめたバージョンであり、現在でもこれらの機能は使えます。</p>
<p>MSAL.js 2.0というMicrosoft製のライブラリはこのPKCE対応をうたっているライブラリなので、このライブラリを使えば、コールバックハンドラーをサーバー側で用意せずとも、ウェブフロントエンドだけで認証が可能となるはずですので、実験してみました。</p>
<p>実現したいことは…</p>
<ul>
<li>MSAL.js (npmのパッケージ名は@azure&#x2F;msal-browser)を組み込む</li>
<li>フロントエンドだけで認証する</li>
</ul>
<p>なお、このライブラリには、ReactとAngular向けのフレームワーク向けのラッパーライブラリが提供されていますが、動きを知るために直接このライブラリを使うものとします。</p>
<h2 id="まずは実験用のサービスをAzureADに登録する">まずは実験用のサービスをAzureADに登録する</h2><p>まずはAzureのActive Directoryのコンソールにアクセスしてアプリケーションを登録します。</p>
<ul>
<li>名前は適当に(azuread-testでもなんでも)</li>
<li>サポートされているアカウントの種類も任意</li>
<li>プラットフォームの種類は <strong>シングルページアプリケーション(SPA)</strong> 、コールバックURLは<code>http://localhost:5173/callback</code>にする（ローカルで動かす開発サーバーで受けるため）</li>
</ul>
<p>なお、すべての項目はあとで修正できますので(アカウント種類はマニフェストエディタでJSONいじる必要があって面倒ですが)、気軽な気持ちで作成できます。また、リダイレクト情報は複数登録できます。</p>
<img fetchpriority="high" src="/images/2022/20221118a/スクリーンショット_2022-11-09_12.34.58.png" alt="スクリーンショット_2022-11-09_12.34.58.png" width="1200" height="818">

<p>作成したあとにアプリケーションを選ぶと、アプリケーションの基本情報が表示されますが、次の2つのUUID型式のIDはあとで大事になります。</p>
<ul>
<li>アプリケーション (クライアント) ID</li>
<li>ディレクトリ (テナント) ID</li>
</ul>
<img src="/images/2022/20221118a/スクリーンショット_2022-11-09_12.37.10.png" alt="スクリーンショット_2022-11-09_12.37.10.png" width="1200" height="710" loading="lazy">

<h2 id="ウェブフロントエンドの作成">ウェブフロントエンドの作成</h2><p>今回はVue.jsで作ってみました。認証部分はプラグイン化して使えるようにします。ReactであればContext化すればよい気がします。Vueのアプリケーションを適当に作ります。僕はVite.jsで作りましたが、vue-cliでもNuxt.jsでもなんでもOKです。</p>
<p>まずはMSAL.jsをインストールします。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">npm install @azure/msal-browser</span><br></pre></td></tr></table></figure>

<p>次に認証情報を設定するファイルを作成します。先ほど作ったアプリケーションの情報を登録します。</p>
<div class="code-block"><figure class="highlight ts"><input type="checkbox" id="code-wrap-1b8jzm7-1" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1b8jzm7-1" title="コードの折り返しを切り替える"></label><figcaption><span>authConfig.ts</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="keyword">type</span> &#123; <span class="title class_">Configuration</span> &#125; <span class="keyword">from</span> <span class="string">&quot;@azure/msal-browser&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="attr">config</span>: <span class="title class_">Configuration</span> = &#123;</span><br><span class="line">    <span class="attr">auth</span>: &#123;</span><br><span class="line">        <span class="attr">authority</span>: <span class="string">&quot;https://login.microsoftonline.com/&#123;テナントID&#125;&quot;</span>,</span><br><span class="line">        <span class="attr">clientId</span>: <span class="string">&quot;&#123;クライアントID&#125;&quot;</span>,</span><br><span class="line">        <span class="attr">redirectUri</span>: <span class="string">&quot;http://localhost:5173/callback&quot;</span></span><br><span class="line">    &#125;,</span><br><span class="line">    <span class="attr">cache</span>: &#123;</span><br><span class="line">        <span class="attr">cacheLocation</span>: <span class="string">&quot;localStorage&quot;</span>,</span><br><span class="line">        <span class="attr">storeAuthStateInCookie</span>: <span class="literal">false</span>,</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure></div>

<p>次にプラグインを作ります。今回は基本的にログインしっぱなしの想定で、未ログインアクセスを許容しない前提でいます。もし、ページによっては未ログインを許可してVue Routerでアクセス制御するのであれば、ログインしているかどうかを確認するメソッド（auth.getAllAccounts()が1つもない）を追加しておけば組み込みがしやすいでしょう。</p>
<div class="code-block"><figure class="highlight ts"><input type="checkbox" id="code-wrap-1b8jzm7-2" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1b8jzm7-2" title="コードの折り返しを切り替える"></label><figcaption><span>authPlugin.ts</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">App</span> &#125; <span class="keyword">from</span> <span class="string">&quot;vue&quot;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">Configuration</span>, <span class="title class_">PublicClientApplication</span>&#125; <span class="keyword">from</span> <span class="string">&quot;@azure/msal-browser&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> <span class="attr">auth</span>: <span class="title class_">PublicClientApplication</span>;</span><br><span class="line"><span class="keyword">let</span> accessToken = <span class="string">&quot;&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">async</span> <span class="keyword">function</span> <span class="title function_">init</span>(<span class="params"><span class="attr">config</span>: <span class="title class_">Configuration</span></span>) &#123;</span><br><span class="line">    auth = <span class="keyword">new</span> <span class="title class_">PublicClientApplication</span>(config);</span><br><span class="line">    <span class="keyword">await</span> auth.<span class="title function_">handleRedirectPromise</span>();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">type</span> <span class="title class_">AuthPluginType</span> = &#123;</span><br><span class="line">    <span class="title function_">login</span>(): <span class="title class_">Promise</span>&lt;<span class="built_in">string</span>&gt;;</span><br><span class="line">    <span class="title function_">logout</span>(): <span class="title class_">Promise</span>&lt;<span class="built_in">void</span>&gt;;</span><br><span class="line">    <span class="title function_">accessToken</span>(): <span class="built_in">string</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">export</span> <span class="keyword">const</span> <span class="title class_">AuthPlugin</span> = &#123;</span><br><span class="line">    <span class="title function_">install</span>(<span class="params"><span class="attr">app</span>: <span class="title class_">App</span>, <span class="attr">config</span>: <span class="title class_">Configuration</span></span>) &#123;</span><br><span class="line">        app.<span class="property">config</span>.<span class="property">globalProperties</span>.<span class="property">$auth</span> = &#123;</span><br><span class="line">            <span class="keyword">async</span> <span class="title function_">login</span>(<span class="params"></span>) &#123;</span><br><span class="line">                <span class="keyword">if</span> (auth.<span class="title function_">getAllAccounts</span>().<span class="property">length</span> &gt; <span class="number">0</span>) &#123;</span><br><span class="line">                    auth.<span class="title function_">setActiveAccount</span>(auth.<span class="title function_">getAllAccounts</span>()[<span class="number">0</span>]);</span><br><span class="line">                    <span class="keyword">const</span> result = <span class="keyword">await</span> auth.<span class="title function_">acquireTokenSilent</span>(&#123;</span><br><span class="line">                        <span class="attr">scopes</span>: [<span class="string">&quot;&#123;クライアントID&#125;/.default&quot;</span>], <span class="comment">// 11/30修正</span></span><br><span class="line">                        <span class="attr">redirectUri</span>: config.<span class="property">auth</span>.<span class="property">redirectUri</span></span><br><span class="line">                    &#125;);</span><br><span class="line">                    accessToken = result.<span class="property">accessToken</span>;</span><br><span class="line">                    <span class="keyword">return</span> accessToken;</span><br><span class="line">                &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">                    <span class="keyword">await</span> auth.<span class="title function_">acquireTokenRedirect</span>(&#123;</span><br><span class="line">                        <span class="attr">redirectStartPage</span>: location.<span class="property">href</span>,</span><br><span class="line">                        <span class="attr">scopes</span>: [<span class="string">&quot;&#123;クライアントID&#125;/.default&quot;</span>], <span class="comment">// 11/30修正</span></span><br><span class="line">                        <span class="attr">redirectUri</span>: config.<span class="property">auth</span>.<span class="property">redirectUri</span></span><br><span class="line">                    &#125;);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;,</span><br><span class="line">            <span class="keyword">async</span> <span class="title function_">logout</span>(<span class="params"></span>) &#123;</span><br><span class="line">                <span class="keyword">await</span> auth.<span class="title function_">logoutRedirect</span>();</span><br><span class="line">            &#125;,</span><br><span class="line">            <span class="title function_">accessToken</span>(<span class="params"></span>) &#123;</span><br><span class="line">                <span class="keyword">return</span> accessToken;</span><br><span class="line">            &#125;,</span><br><span class="line">        &#125;;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure></div>

<p>この<code>login()</code>メソッドが肝です。</p>
<ul>
<li>すでにログイン済みの場合は<code>PublicClientApplication.getAllAccounts()</code>がユーザー一覧を返すのでアクティブユーザーとして設定する。本当はidTokenClaims.audがクライアントIDと一致しているものを探すというのを丁寧にやった方がいいかもしれないけど、今回のケースではそもそも違うクライアントIDのユーザーアカウントがここに入ることは今のところないので雑に最初の要素をピック。</li>
<li><code>PublicClientApplication.acquireTokenSilent()</code>はログイン済みであればアクセストークンをAzureADに問い合わせることなく取得してVue.js内で使える形で返すが、未ログインだと例外を投げる。</li>
<li>未ログインだった場合に<code>PublicClientApplication.acquireTokenRedirect()</code>を使ってAzureADにリダイレクトしてログインを行う。</li>
</ul>
<p>いろいろ試行錯誤しましたが、たぶんこれが最小ケースです。</p>
<p>なお、MSAL.jsにはリダイレクトモードだけでなく、SPA向けのポップアップモードがありますが、Chromeでは動かず、Edgeでしか動きませんでした。そもそも別ウィンドウでログイン画面が出るため、未ログイン時の画面のブロックとかを実装するのは手間なので、今回紹介したリダイレクトモードの方が手間が少なくて済むかと思います。あと、ChromeもEdgeも、デフォルトで別ウィンドウのポップアップはブロックされるという問題もあります。今のところ選ぶ理由が見当たらないです。</p>
<p>**(11&#x2F;30追記)**なお、スコープは<code>[&#39;&#123;クライアントID&#125;/.default&#39;]</code>は<code>[&quot;User.Read&quot;]</code>でもトークンは取得できるのですが、生成されたアクセストークンをvalidationすると必ずエラーになってしまいます。アクセストークンをサーバー側で検証することでログインが正常に行われたかどうかを判定するのがログインの肝なので、このスコープ名には注意してください。Microsoftの提供するAPIを利用する場合はその該当するAPIをスコープにすればOKです。この場合は自分での検証はできません。詳しくは以下のリンク先を参照してください。</p>
<ul>
<li>https://stackoverflow.com/questions/45317152/invalid-signature-while-validating-azure-ad-access-token-but-id-token-works</li>
</ul>
<p>TypeScript用にプラグインの型定義も書いておきます。</p>
<div class="code-block"><figure class="highlight ts"><input type="checkbox" id="code-wrap-1b8jzm7-3" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1b8jzm7-3" title="コードの折り返しを切り替える"></label><figcaption><span>auth.d.ts</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> <span class="keyword">type</span> &#123; <span class="title class_">AuthPluginType</span> &#125; <span class="keyword">from</span> <span class="string">&quot;./authPlugin&quot;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">declare</span> <span class="variable language_">module</span> <span class="string">&quot;vue&quot;</span> &#123;</span><br><span class="line">  <span class="keyword">interface</span> <span class="title class_">ComponentCustomProperties</span> &#123;</span><br><span class="line">    <span class="attr">$auth</span>: <span class="title class_">AuthPluginType</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div>

<h2 id="組み込み">組み込み</h2><p><code>PublicClientApplication</code>の初期化はVue.jsとかよりも先に行います。</p>
<div class="code-block"><figure class="highlight ts"><input type="checkbox" id="code-wrap-1b8jzm7-4" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1b8jzm7-4" title="コードの折り返しを切り替える"></label><figcaption><span>main.ts</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">import</span> &#123; createApp &#125; <span class="keyword">from</span> <span class="string">&#x27;vue&#x27;</span></span><br><span class="line"><span class="keyword">import</span> <span class="title class_">App</span> <span class="keyword">from</span> <span class="string">&#x27;./App.vue&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; <span class="title class_">AuthPlugin</span>, init &#125; <span class="keyword">from</span> <span class="string">&#x27;./authplugin&#x27;</span>;</span><br><span class="line"><span class="keyword">import</span> &#123; config &#125; <span class="keyword">from</span> <span class="string">&#x27;./authconfig&#x27;</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">await</span> <span class="title function_">init</span>(config);</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> app = <span class="title function_">createApp</span>(<span class="title class_">App</span>);</span><br><span class="line">app.<span class="title function_">use</span>(<span class="title class_">AuthPlugin</span>, config);</span><br><span class="line">app.<span class="title function_">mount</span>(<span class="string">&#x27;#app&#x27;</span>);</span><br></pre></td></tr></table></figure></div>

<p>MSAL.jsは初期化時に、AzureADからのコールバックでフロントエンドが呼ばれた場合の、クエリー文字列にOAuthの認証コードが付与されている場合には、「コールバックが来たぞ！」と検知して、OpenID Connectの認証処理の続きを行ってくれます。そして、その後<code>redirectStartPage</code>で指定したURLにリダイレクトまでやってくれます。そのために、<code>init()</code>で待ち（正確には<code>auth.handleRedirectPromise()</code>を待つ）を入れています。<code>await</code>を忘れるとエラーが出て認証に失敗します。</p>
<p>コンポーネントへの組み込みは以下の通りです。プラグインで作成した<code>login()</code>と<code>logout()</code>を呼び出せるようにしています。あとは、アクセストークンもプラグイン経由で取得できますので、あとはこれをサーバーAPIリクエスト時にヘッダーに設定すればOKです。</p>
<div class="code-block"><figure class="highlight html"><input type="checkbox" id="code-wrap-1b8jzm7-5" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1b8jzm7-5" title="コードの折り返しを切り替える"></label><figcaption><span>App.vue</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">script</span> <span class="attr">lang</span>=<span class="string">&quot;ts&quot;</span>&gt;</span><span class="language-javascript"></span></span><br><span class="line"><span class="language-javascript"><span class="keyword">export</span> <span class="keyword">default</span> &#123;</span></span><br><span class="line"><span class="language-javascript">  <span class="attr">methods</span>: &#123;</span></span><br><span class="line"><span class="language-javascript">    <span class="title function_">logout</span>(<span class="params"></span>) &#123;</span></span><br><span class="line"><span class="language-javascript">      <span class="variable language_">this</span>.<span class="property">$auth</span>.<span class="title function_">logout</span>();</span></span><br><span class="line"><span class="language-javascript">    &#125;</span></span><br><span class="line"><span class="language-javascript">  &#125;,</span></span><br><span class="line"><span class="language-javascript">  <span class="keyword">async</span> <span class="title function_">created</span>(<span class="params"></span>) &#123;</span></span><br><span class="line"><span class="language-javascript">    <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">$auth</span>.<span class="title function_">login</span>();</span></span><br><span class="line"><span class="language-javascript">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="variable language_">this</span>.<span class="property">$auth</span>.<span class="title function_">accessToken</span>());　<span class="comment">// アクセストークン表示</span></span></span><br><span class="line"><span class="language-javascript">  &#125;</span></span><br><span class="line"><span class="language-javascript">&#125;</span></span><br><span class="line"><span class="language-javascript"></span><span class="tag">&lt;/<span class="name">script</span>&gt;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="name">template</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">div</span>&gt;</span></span><br><span class="line">    login test</span><br><span class="line">  <span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br><span class="line">  <span class="tag">&lt;<span class="name">button</span> <span class="attr">v-on:click</span>=<span class="string">&quot;logout&quot;</span>&gt;</span>logout<span class="tag">&lt;/<span class="name">button</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="name">template</span>&gt;</span></span><br></pre></td></tr></table></figure></div>

<h2 id="デバッグ">デバッグ</h2><p>結構はまったのですが、ログインの途中でとまったときは、セッションストレージにゴミが残ります。この状態でMSAL.jsのAPIを呼んでも、処理中のものがあるというエラーになってしまうので、ブラウザを再起動するか、開発者ツールでセッションストレージを掃除します。</p>
<p>あとは、 <code>auth.handleRedirectPromise()</code> のPromiseのエラーとか返り値を見てみるのも良いです。</p>
<h2 id="セキュリティ強度を変えるためのチューニング">セキュリティ強度を変えるためのチューニング</h2><p>今回は1人1台専用のマシンがある前提のコードになっているため、ブラウザ再起動でもセッションが残るようにlocalStorageに入れていますが、そうでない場合はブラウザを落としたら認証情報もリセットされるようにsessionStorageにしてあげた方が良いでしょう。上記のサンプルコードの<code>authConfig.ts</code>で変更できます。</p>
<p>あとはこの形式だとサーバーを介さずにフロントだけで認証するため、サーバー側からアクセスの無効化などができません。フロントで作ったトークンをサーバーに送って「使っていいよ」というお墨付きを与える（あるいはユーザーごとに1セッションしか認めず、後からログインしたら先のログインは無効）みたいなロジックとかを作ればそのような問題には対処できるかもしれませんが、それであればフロントエンドだけで認証という方式ではなく、最初からアクセストークンの発行はサーバーに任せた方が良い気もします。</p>
<h2 id="まとめ">まとめ</h2><p>PKCEの恩恵で、サーバーいらずの認証が実装できました。サーバーで認証する場合、サーバー側に設定を入れる必要があり、ダメだった場合に何度もデプロイしてテストしたり不便でしたが、とても簡単に実装できました。</p>
<p>サーバー側としては、JWTの検証だけは必要となりますので、そこだけ実装が必要です。MSAL.jsのリポジトリにサーバー側でのトークン検証のサンプル(Express利用)があるので、見てみると良いでしょう。</p>
]]></content>
    <summary type="html">AzureADを使って認証している企業は多いと思います。このAzureADを使った場合にはMSAL.jsを使えば認証は楽だぞ、というのはAzureADのサイトには書かれているのですが、OpenID Connectのプロトコルの動きの理解と、ライブラリのAPIがどう対応づいているのかがわからずにちょっと試行錯誤したので、そのメモを残しておきます。</summary>
    <category term="認証認可" scheme="https://future-architect.github.io/categories/%E8%AA%8D%E8%A8%BC%E8%AA%8D%E5%8F%AF/"/>
    <category term="AD" scheme="https://future-architect.github.io/tags/AD/"/>
    <category term="Azure" scheme="https://future-architect.github.io/tags/Azure/"/>
    <category term="EntraID" scheme="https://future-architect.github.io/tags/EntraID/"/>
    <category term="MSAL.js" scheme="https://future-architect.github.io/tags/MSAL-js/"/>
  </entry>
  <entry>
    <title>パスワードレス技術の現状と未来について</title>
    <link href="https://future-architect.github.io/articles/20221114a/"/>
    <id>https://future-architect.github.io/articles/20221114a/</id>
    <published>2022-11-13T15:00:00.000Z</published>
    <updated>2022-11-13T15:00:00.000Z</updated>
    <author><name>吉岡朋哉</name></author>
    <content type="html"><![CDATA[<h2 id="はじめに">はじめに</h2><p>こんにちは。TIG の吉岡です。秋のブログ週間 10 本目の投稿です。</p>
<p>2022年の 5 月に Apple, Google, Microsoft そして FIDO Alliance が マルチデバイス対応FIDO認証資格情報 を発表してから、パスワードレス技術に対する注目が高まっています。<sup id="fnref:1">1</sup> パスワードレスの概要について調査してまとめてみました。</p>
<h2 id="私たちとパスワード">私たちとパスワード</h2><p>今日、私たちのデジタルアイデンティティはパスワードに支えられています。私たちは日々 Google で検索し、Netflix を観て、Twitter でつぶやき、Amazon で買い物をしますが、これらすべてのアカウントが、パスワードによって保護されています。</p>
<p>パスワードは通常 TLS によって安全にクライアントからサーバに転送され、サーバ上で難読化されて保存されるため、攻撃者が任意のパスワードを即座に奪取することは困難です。しかしながら、秘密鍵であるパスワードを他者と共有する方法は、本質的で避けることのできない問題を複数孕みます。</p>
<h3 id="パスワードの抱える問題">パスワードの抱える問題</h3><ol>
<li>ユーザがパスワードを適切に管理するのは困難である</li>
<li>サーバや通信経路からパスワードが漏洩することがある</li>
<li>パスワードにはフィッシング耐性がない</li>
</ol>
<h4 id="ユーザがパスワードを適切に管理するのは困難である">ユーザがパスワードを適切に管理するのは困難である</h4><p>IPA によると、パスワードは、できるだけ長く、複雑で、使い回さないものとすべきだそうです。<sup id="fnref:2">2</sup>それはそうなのですが、このベストプラクティスを人間が実践することは事実上不可能です。私たちは数百のアカウントを保持しています。そのアカウント全てに対して、ユニークでランダムな文字列を記憶できないでしょう。残念なことに、複数のサービスでパスワードを使い回しているユーザも多くいるようです。</p>
<h4 id="サーバや通信経路からパスワードが漏洩することがある">サーバや通信経路からパスワードが漏洩することがある</h4><p>ユーザがパスワードを適切に管理していたとしても、通信経路やサーバにセキュリティ上の不備がある場合、パスワードが漏洩し、ユーザのアカウントが危険に晒される場合があります。通常、パスワードは難読化された上でデータベースに保存されますが、平文で保存されたパスワードが漏洩した事例 も存在します。ユーザにとって、ウェブサービスが適切に管理・運営されていることを断定的に知る術はなく、パスワードの使用には本質的な危険が伴います。</p>
<h4 id="パスワードにはフィッシング耐性がない">パスワードにはフィッシング耐性がない</h4><p>パスワードは、人間であるユーザが直接入力できる文字列であり、フィッシング耐性がありません。普段からフィッシング詐欺の被害に遭わないよう注意している人であっても、急いでいるときや慌てているときに、適切に入力先ウェブサイトの真正性を確かめるよう徹底することは、簡単なことではありません。また、ミスリード URL やホモグラフ攻撃 に常に気をつける必要があるというのは、それだけでも大きな心理的負担です。パスワードには本質的にフィッシング耐性がなく、人間の注意力に期待するのは無謀でしょう。</p>
<h3 id="パスワードマネージャ">パスワードマネージャ</h3><p>このようなパスワードの惨状に際して、パスワードマネージャを利用することが、現時点でのベストプラクティスであり、唯一のまともな解決策であると筆者は考えています。プラットフォーマーとして Apple は iCloud Keychain を、Google は Google Password Manager を提供していますし、サードパーティとしても 1Password や Bitwarden、Dashlane のような有力なプレイヤーが存在します。</p>
<p>ユーザは信頼できるパスワードマネージャを適切に使用することで、パスワードに潜む問題をある程度解消できます。パスワードマネージャはユニークでランダムなパスワードを作成・管理でき、また、常にオートフィル機能を使うようにすれば、フィッシング被害を受ける可能性も大きく下げることができます。</p>
<p>ところがパスワードマネージャも銀の弾丸ではありません。オートフィルが活用できないような場面では相変わらずフィッシングの被害を受ける可能性がありますし、サーバからパスワードが漏洩するような事態に対しても、ユーザは無防備のままです。</p>
<h3 id="公開鍵暗号の活用">公開鍵暗号の活用</h3><p>根本的な問題は、秘密鍵であるパスワードをクライアントとサーバが共有する、という現状のモデルにあります。トランスポート層では 10 年以上前から SSL&#x2F;TLS として公開鍵暗号が大活躍しているのに、アプリケーション層で同様の技術を活用しない手はありません。公開鍵暗号を活用し、パスワードへの過度な依存を軽減するため、2012 年に Fido Alliance が設立されました。</p>
<h2 id="パスワードレスと-FIDO-Alliance">パスワードレスと FIDO Alliance</h2><p>FIDO Alliance は、パスワードに対する依存を軽減するため、PayPal や Lenovo らにより 2012 年に結成された業界団体です。現在では Amazon, Apple, Google, Microsoft などが参加する一大アライアンスに成長しています。FIDO は、TPM や生体認証機能を備えた認証器 (スマートフォンやセキュリティキー) を活用し、ユーザがパスワードを利用することなくアカウントにログインできるようにすることを目的としており、そのための標準規格をいくつか定めています。</p>
<p>FIDO が発表した重要な規格には、FIDO 1.0 (2014) と FIDO2 (2018) があります。</p>
<h3 id="FIDO-v1-0">FIDO v1.0</h3><p>FIDO v1.0 は FIDO UAF (Universal Authentication Framework) と FIDO U2F (Universal 2nd Factor) から成ります。FIDO UAF はスマートフォンのネイティブアプリケーション向けに、公開鍵ベースのパスワードレス認証を規定します。FIDO U2F は、パスワードに加わる第 2 認証要素として、従来通りの OTP でなく、公開鍵暗号を利用できるようにした仕様です。</p>
<p>この UAF と U2F ですが、仕様編纂者を見るに、UAF は PayPal が、U2F は Google が主体となって仕様策定を進めたようで、全体として足並みが揃っていない感があります。FIDO として一貫性のある仕様の実現には、FIDO2 を待つ必要がありました。</p>
<h3 id="FIDO2">FIDO2</h3><p>FIDO v1.0 にはいくつかの反省点がありました。UAF は半ばスマートフォンのネイティブアプリケーションで使うことを前提としていたため、ウェブブラウザへの応用がすすみませんでしたし、U2F はあくまで従来のパスワードを補完する技術要素にすぎず、完全なパスワードレスを実現するものではありませんでした。これらの問題を解決した最新の FIDO 仕様が、2018 年に発表された FIDO2 です。</p>
<p>FIDO2 は WebAuthn と CTAP から成る公開鍵ベースの認証技術仕様です。</p>
<h4 id="WebAuthn">WebAuthn</h4><p>WebAuthn (Web Authentication) は、FIDO Alliance と W3C の共同作業として、2016 から作業が開始し、2019 年にウェブ標準となった仕様です。WebAuthn はウェブブラウザが認証器とコミュニケーションをとり、キーペアを作成したり、チャレンジに署名したりする方法を定めています。この仕様により、ウェブ開発者はユーザの認証器に対してキーペアの作成やチャレンジへの署名を依頼できます。2022 年 11 月現在、WebAuthn は Firefox を除くすべての主要なブラウザで完全にサポートされています。<sup id="fnref:3">3</sup></p>
<h4 id="CTAP">CTAP</h4><p>CTAP (Client to Authenticator Protocol) は、OS がセキュリティキーのような外部認証器とやりとりする際の低レイヤープロトコルを規定しています。ウェブ開発者が普段意識しないような、ウェブブラウザよりも先にある世界のプロトコルです。<sup id="fnref:4">4</sup></p>
<h3 id="FIDO-の認証フロー">FIDO の認証フロー</h3><p>FIDO は複数の仕様を規定しており、それらの関係が複雑なのですが、<strong>公開鍵暗号を利用した認証プロトコル</strong> であるという点は、すべてに共通しています。おおまかに言って、FIDO のパスワードレス認証は次のようなフローを採用しています。</p>
<h4 id="ユーザ登録">ユーザ登録</h4><p>FIDO 認証のユーザ登録時にはユーザの認証器がキーペアを作成し、公開鍵をサーバに送信します。サーバは公開鍵を保存します。サーバは秘密鍵を知らないため、仮にサーバから情報が漏洩しても、第三者がユーザのアカウントを乗っ取ることはできません。</p>
<img fetchpriority="high" src="/images/2022/20221114a/register.png" alt="registerシーケンス" width="456" height="330">

<h4 id="ユーザ認証-サインイン">ユーザ認証 (サインイン)</h4><p>ユーザ認証 (サインイン) 時にはサーバがランダムなチャレンジを生成し、クライアントに送信します。クライアントは秘密鍵でチャレンジに署名し、サーバに返却します。サーバはユーザ登録時に保存していた公開鍵で署名を検証し、有効な署名であれば、ユーザをサインインさせます。なお、このときサインイン先のドメイン名がユーザ登録したドメイン名と同一であることがクライアント側で検証されるため、FIDO 認証にはフィッシング耐性があります。</p>
<img src="/images/2022/20221114a/signin.png" alt="signinシーケンス" width="549" height="372" loading="lazy">

<h3 id="Passkeys">Passkeys</h3><p>FIDO Alliance の設立から 10 年近くを経て、ベンダー中立な FIDO2 仕様群が策定され、多くの OS・ブラウザでサポートされるようになりましたが、この技術が一般に広く用いられるには、移行とリカバリの問題が残っていました。従来の FIDO 認証では、ユーザの秘密鍵はデバイスのセキュアストレージを出ることなく、ローカルに保存されていました。従って、ユーザが複数のデバイスを使用しているとき、デバイスごとにサービスに登録する必要がありました。また、デバイスを買い替えたとき、アカウントをシームレスに移行する機能はなく、すべてのアカウントについて、再登録が必要でした。さらに怖いことに、デバイスを紛失したり破損したりしてしまうと、アカウントに対するアクセスを完全に失ってしまう可能性がありました。</p>
<p>この問題を解決し、パスワードレス技術を真にユビキタスなものにするため、2022 年 5 月 5 日 (World Password Day) に、Apple, Google, Microsoft, FIDO Alliance が共同で声明を発表し、マルチデバイス対応 FIDO 認証資格情報 (通称 passkeys) への対応を推進していくことを宣言しました。Passkeys により、iCloud Keychain や Google Password Manager を通して秘密鍵をデバイス間で安全に同期でき、移行とリカバリの問題も解消されます。Passkeys は macOS 13 Ventura や iOS 16 の Safari 16 ですでにサポートされており、Google も Android と Chrome で今秋に対応 することを発表しています。</p>
<h2 id="パスワードレスな未来">パスワードレスな未来</h2><p>現状、広範に採用されているとは言い難い FIDO, WebAuthn, passkeys ですが、強力なプラットフォーマーが協力して推進していくことから、今後採用が進んでいくことが考えられます。直近では Apple, Google そして Microsoft といったプラットフォーマーによるサポートから始まっていますが、将来的には 1Password や Dashlane のようなパスワードマネージャも認証器機能を提供する予定だそうです。ユーザとサービス提供者をパスワードから解放するパスワードレス技術に今後も注目していきます。</p>
<div id="footnotes"><hr><div id="footnotelist"><ol style="list-style:none; padding-left: 0;"><li id="fn:1"><span style="vertical-align: top; padding-right: 10px;">1.</span><span style="vertical-align: top;">当社比</span> ↩</li><li id="fn:2"><span style="vertical-align: top; padding-right: 10px;">2.</span><span style="vertical-align: top;">ちなみに、IPA のウェブサイトで紹介されている「コアパスワード」を使った管理方法を採用することはお勧めしません。パスワードが平文で漏洩したとき、プレフィックスを識別するのが容易で、コアパスワードが攻撃者に奪取されるためです。</span> ↩</li><li id="fn:3"><span style="vertical-align: top; padding-right: 10px;">3.</span><span style="vertical-align: top;">&quot;WebAuthn&quot; | Can I use... Support tables for HTML5, CSS3, etc</span> ↩</li><li id="fn:4"><span style="vertical-align: top; padding-right: 10px;">4.</span><span style="vertical-align: top;">FIDO2 に含まれるのは CTAP2 と呼ばれる仕様です。FIDO v1.0 において FIDO U2F と呼ばれていたものは、FIDO2 において CTAP1 に改名されました。</span> ↩</li></ol></div></div>]]></content>
    <summary type="html">2022年の 5 月に Apple, Google, Microsoft そして FIDO Alliance が マルチデバイス対応FIDO認証資格情報 を発表してから、パスワードレス技術に対する注目が高まっています。パスワードレスの概要について調査してまとめてみました。</summary>
    <category term="認証認可" scheme="https://future-architect.github.io/categories/%E8%AA%8D%E8%A8%BC%E8%AA%8D%E5%8F%AF/"/>
    <category term="WebAuthn" scheme="https://future-architect.github.io/tags/WebAuthn/"/>
    <category term="パスキー" scheme="https://future-architect.github.io/tags/%E3%83%91%E3%82%B9%E3%82%AD%E3%83%BC/"/>
  </entry>
  <entry>
    <title>OAuth の仕組みを理解しながらクライアントを実装してみる</title>
    <link href="https://future-architect.github.io/articles/20221012a/"/>
    <id>https://future-architect.github.io/articles/20221012a/</id>
    <published>2022-10-11T15:00:00.000Z</published>
    <updated>2022-10-11T15:00:00.000Z</updated>
    <author><name>吉岡朋哉</name></author>
    <content type="html"><![CDATA[<h2 id="はじめに">はじめに</h2><p>こんにちは、TIG の吉岡と申します。Tech Blog には初投稿です。認証認可連載の 5 本目です。</p>
<p>業務で認証・認可に関する SaaS に触れる場面があり、そういえば OAuth, OpenID Connect の仕組みをちゃんと理解していなかったと思い、RFC を読みながら OAuth クライアントを実装してみました。N 番煎じの車輪の再発明ですが、何かの役に立てば幸いです。</p>
<p>※本稿において、OAuth は基本的に OAuth 2.0 を指しますが、OAuth 2.1 にて推奨されているベストプラクティスを取り入れています。<br>※本当は OpenID Connect にも触れたかったのですが力尽きました。また機会があれば書かせてください。</p>
<h2 id="Prerequisites">Prerequisites</h2><p>この記事は、次のような方々に向けて書いています。</p>
<ul>
<li>OAuth を聞いたことがある</li>
<li>認証と認可の違いを認識している</li>
<li>HTTP の基本的な仕組みを知っている</li>
</ul>
<h2 id="OAuth-とは">OAuth とは</h2><p>OAuth は、サードパーティアプリケーションが HTTP サービスに対して制限付きのアクセスを取得することを可能にする認可フレームワークです。</p>
<p>具体的には、ある SNS アプリケーションがあり、ユーザーはこの SNS に対して、自身の Google Photos 上の画像を投稿したいとします。このとき、SNS アプリケーションに Google のユーザー名とパスワードを教えることにより、画像を取得させることもできます。しかし、この方法には、付与される権限が大きすぎる、権限を剥奪するためにはパスワードを変更するしかないなど、さまざまな問題があります。</p>
<p>これらの問題を解決し、サードパーティアプリケーションに適切な権限を付与するための認可フレームワークが OAuth です。</p>
<p>なお、2022 年現在広く使われている仕様である OAuth 2.0 は RFC 6749 により規定されています。この仕様には、後にさまざまな拡張が施されたため、それらの拡張とセキュリティに関するベストプラクティスをまとめた OAuth 2.1 が策定中です。OAuth 2.1 は OAuth 2.0 とその拡張をまとめ直した仕様として策定中であり、OAuth 2.0 を大きく変えるものではありません。</p>
<p>本稿では、OAuth 2.0 並びに OAuth 2.1 における代表的なフローである「認可コードグラント + PKCE」を解説します。</p>
<h2 id="OAuth-のロール">OAuth のロール</h2><p>OAuth では、次の 4 つのロール (登場人物) が定義されています。</p>
<ul>
<li>リソースオーナー: リソースの所有者であるエンドユーザー</li>
<li>リソースサーバー: リソースを保持しているサーバー</li>
<li>クライアント: リソースオーナーが利用し、リソースに対する権限を付与されるアプリケーション</li>
<li>認可サーバー: リソースオーナーの承諾を得たうえでクライアントに対してアクセストークンを発行するサーバー</li>
</ul>
<p>4 つのロールのうち、クライアントには注意が必要です。通常私たちがクライアントと聞くと、OS のネイティブアプリケーションやブラウザ上で動作するアプリケーションを想像してしまいますが、OAuth の言葉遣いにおいては、それらに限らず、サーバー上で動作するウェブアプリケーションもクライアントに含まれます。</p>
<p>また、クライアントは <strong>コンフィデンシャルクライアント</strong> と <strong>パブリッククライアント</strong> の 2 種類に分類されます。後に説明しますが、認可サーバーはクライアントを識別するために、クライアント ID とクライアントシークレットを発行します。このクライアントシークレットを安全に保持できるクライアントはコンフィデンシャルクライアント、安全に保持できないクライアントはパブリッククライアントと呼ばれます。サーバー上で動作するウェブアプリケーションはコンフィデンシャルクライアントで、OS のネイティブアプリケーションやブラウザ上で動作するアプリケーションはパブリッククライアントです。</p>
<p>なお、クライアントは事前に認可サーバーに登録されている必要があります。登録の方法は OAuth 仕様の範疇外ですが、多くの場合、ウェブアプリケーションとして構築されたマネジメントコンソールなどから登録できます。本稿では Google API にクライアントを登録するフローを説明します。</p>
<h2 id="OAuth-のグラントタイプ">OAuth のグラントタイプ</h2><p>OAuth では、権限付与の種類が 4 種類規定されており、これをグラントタイプと呼びます。</p>
<ul>
<li>認可コードグラント + PKCE</li>
<li>インプリシットグラント (非推奨)</li>
<li>リソースオーナーパスワードクレデンシャルグラント (非推奨)</li>
<li>クライアントクレデンシャルグラント</li>
</ul>
<p>認可コードグラント + PKCE は、エンドユーザーの承諾を得た上でクライアントがリソースにアクセスするという、OAuth の典型的なパターンです。クライアントクレデンシャルグラントは、クライアントが直接、クライアント自身の認証情報を認可サーバーに提示して、リソースへのアクセスを取得する方法です。</p>
<p>OAuth 2.0 では、これらに加えてインプリシットグラント、リソースオーナーパスワードクレデンシャルグラントが規定されていますが、これらは OAuth 2.1 で削除される予定であり、もはや利用すべきではありません。</p>
<p>本稿では <strong>認可コードグラント + PKCE</strong> による権限付与を説明します。認可コードグラントは、リソースオーナー・クライアント・認可サーバーの三者のやりとりによって認可を達成するため、<strong>3-legged OAuth</strong> などとも呼ばれます。</p>
<h2 id="OAuth-のシーケンス">OAuth のシーケンス</h2><p>認可コードグラントによる OAuth の処理シーケンスは図のとおりです。</p>
<img fetchpriority="high" src="/images/2022/20221012a/oauth.png" alt="oauth.png" width="587" height="501">

<ol>
<li>ユーザーが「Google Photos から画像を取得する」ボタンを押下する</li>
<li>クライアントが 302 Found を返却し、ユーザーエージェントを認可サーバーの認可エンドポイントにリダイレクトする <sup id="fnref:1">1</sup></li>
<li>ユーザーエージェントが認可サーバーの認可エンドポイントにアクセスする</li>
<li>認可サーバーが認証画面を表示し、ユーザーにログインを促す</li>
<li>ユーザーが認証情報を入力し、認可サーバーに認証を求める</li>
<li>認証に成功した認可サーバーは、クライアントが要求する権限一覧をユーザーに提示し、ユーザーの承諾を求める</li>
<li>ユーザーがクライアントに対する権限の付与を承諾する</li>
<li>権限付与の承諾を得た認可サーバーは 302 Found を返却し、ユーザーエージェントをクライアントのリダイレクト URI にリダイレクトする <sup id="fnref:1">1</sup></li>
<li>ユーザーエージェントがリダイレクト URI にアクセスする</li>
<li>クライアントは手順 9 で取得した認可コードを用いて認可サーバーにトークンリクエストを送信する</li>
<li>リクエストの有効性を検証した認可サーバーは、アクセストークンを発行し、トークンレスポンスを返却する</li>
<li>クライアントは手順 11 で取得したアクセストークンを用いてリソースサーバーにアクセスする</li>
<li>アクセストークンの有効性を検証したリソースサーバーは、リソースを返却する</li>
<li>クライアントはリソースオーナーのユーザーエージェントに対して、リソースを含む画面をレスポンスする</li>
</ol>
<p>順に詳しく見ていきます。</p>
<h3 id="Step-A-手順-1…2">Step A. 手順 1…2</h3><p>手順 1 から 2 では、ユーザーをクライアントから認可サーバーに誘導します。ユーザーがアプリケーションの「Google Photos から画像を取得する」などのボタンをクリックすると、クライアントは各種パラメータを生成し、302 Found をレスポンスします。これにより、ユーザーエージェントは認可サーバーにリダイレクトされます。この認可サーバーに対するリクエストを <strong>認可リクエスト</strong> と呼び、リクエスト先を <strong>認可エンドポイント</strong> と呼びます。</p>
<p>認可リクエストは次の通りです。ここで、認可サーバーの認可エンドポイントは <code>auth.example.com/authorize</code> であるとします。</p>
<div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-172paf6-1" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-172paf6-1" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">GET /authorize</span><br><span class="line">    ?response_type=code                                     <span class="comment"># 1</span></span><br><span class="line">    &amp;client_id=&lt;client_id&gt;                                  <span class="comment"># 2</span></span><br><span class="line">    &amp;state=&lt;state&gt;                                          <span class="comment"># 3</span></span><br><span class="line">    &amp;scope=&lt;scope&gt;                                          <span class="comment"># 4</span></span><br><span class="line">    &amp;redirect_uri=https://client.example.com/callback       <span class="comment"># 5</span></span><br><span class="line">    &amp;code_challenge=&lt;code_challenge&gt;                        <span class="comment"># 6</span></span><br><span class="line">    &amp;code_challenge_method=&lt;code_challenge_method&gt; HTTP/1.1 <span class="comment"># 7</span></span><br><span class="line">Host: auth.example.com</span><br></pre></td></tr></table></figure></div>

<ol>
<li>(必須) 認可コードグラントを利用するため、<code>response_type=code</code> を指定します</li>
<li>(必須) クライアント登録時に認可サーバーから発行された <code>client_id</code> を指定します</li>
<li>(推奨) CSRF 攻撃を防ぐため、ランダムな <code>state</code> 値を指定します</li>
<li>(任意) リソースサーバー・認可サーバーが規定するリソースアクセスのスコープを指定します</li>
<li>(任意) クライアントが認可レスポンスを受け取るための URI を指定します</li>
<li>(推奨) ランダムに生成した <code>code_verifier</code> から生成されたチャレンジを指定します (PKCE)</li>
<li>(推奨) <code>code_challenge</code> を生成する方法を指定します (PKCE)</li>
</ol>
<p>上の例では、認可コード横取り攻撃と呼ばれる攻撃を防ぐため、 <strong>PKCE (Proof Key for Code Exchange)</strong> という仕組みを使用しています。PKCE は、OAuth 2.0 において使用が推奨されており、OAuth 2.1 においては事実上必須となる見込みです。</p>
<p>ここで、PKCE を用いる場合、クライアントはまず <code>code_verifier</code> と呼ばれるランダムな ASCII 文字列を生成します。<code>code_verifier</code> を正規表現で書くと <code>/[A-Za-z0-9-_.~]&#123;43,128&#125;/</code> です。次に、クライアントは <code>code_challenge_method</code> の値を選択します。<code>code_challenge_method</code> として有効な値は <code>plain</code> と <code>S256</code> です。<code>code_challenge_method</code> の値により、<code>code_challenge</code> はそれぞれ次のように計算されます。</p>
<div class="scroll"><table>
<thead>
<tr>
<th>code_challenge_method</th>
<th>code_challenge</th>
</tr>
</thead>
<tbody><tr>
<td>plain</td>
<td><code>code_verifier</code></td>
</tr>
<tr>
<td>S256</td>
<td><code>BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))</code></td>
</tr>
</tbody></table></div>
<p><code>code_challenge_method == plain</code> のとき、<code>code_challenge</code> と <code>code_verifier</code> は同一の値です。<code>code_challenge_method == S256</code> のとき、<code>code_challenge</code> は <code>code_verifier</code> を SHA-256 ハッシュ関数に通した値となります。特段の技術的な制約がない限り、<code>code_challenge_method == S256</code> を指定すべきです。</p>
<h3 id="Step-B-手順-3…8">Step B. 手順 3…8</h3><p>手順 3 から 8 では、ユーザーは認可サーバーとコミュニケーションをとります。手順 4, 5 においてユーザーは認可サーバーにログインします。この認証は、あくまで認可サーバーが認可処理を進めるために必要な認証であり、クライアントは認証情報そのものはおろか、認証情報がやりとりされていることすら知ることはありません。また、ユーザーがすでにユーザーエージェント上で対象サービスにログインしており、そのセッションが有効な場合、手順 4, 5 は省略されることが一般的です。手順 6, 7 において、ユーザーはクライアントに対する権限の付与を提示され、問題がなければそれを承諾します。権限付与の承諾を確認した認可サーバーは、手順 8 で <strong>認可コード</strong> を発行し、これをクエリパラメータに付与して <strong>リダイレクト URI</strong> に対する 302 Found をレスポンスします。</p>
<p>認可レスポンスは次のとおりです。<code>Location</code> ヘッダにはリダイレクト URI が含まれています。ここで、クライアントのリダイレクト URI は <code>https://client.example.com/callback</code> であるとします。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">HTTP/1.1 302 Found</span><br><span class="line">Location: https://client.example.com/callback</span><br><span class="line">    ?code=&lt;authorization_code&gt;                <span class="comment"># 1</span></span><br><span class="line">    &amp;state=&lt;state&gt;                            <span class="comment"># 2</span></span><br></pre></td></tr></table></figure>

<ol>
<li>(必須) 認可サーバーが発行した認可コード</li>
<li>(認可リクエストに <code>state</code> が含まれる場合必須) 認可リクエストにて送信した <code>state</code> 値</li>
</ol>
<p>なお、手順 8 と手順 9 の間において、認可リクエストと認可レスポンスの <code>state</code> 値を比較し、これらが等しくない場合は CSRF 攻撃が行われたと判断して処理を中断する必要があります。</p>
<h3 id="Step-C-手順-9…14">Step C. 手順 9…14</h3><p>手順 8 により 302 Found を受け取ったユーザーエージェントは、手順 9 でリダイレクト URI に遷移します。ここにおいてクライアントは認可コードを取得し、手順 10 で <strong>トークンリクエスト</strong> を発行します。なお、トークンリクエストを受け取る認可サーバーのエンドポイントは <strong>トークンエンドポイント</strong> と呼ばれます。手順 11 において、認可コードの有効性を確認した認可サーバーは <strong>アクセストークン</strong> を発行し、<strong>トークンレスポンス</strong> としてクライアントに返却します。これ以降、クライアントはアクセストークンを用いてリソースサーバーにアクセスし、リソースへの CRUD 操作ができるようになります。</p>
<p>トークンリクエストは次の通りです。ここで、認可サーバーのトークンエンドポイントは <code>auth.example.com/token</code> であるとします。</p>
<div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-172paf6-2" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-172paf6-2" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">POST /token HTTP/1.1</span><br><span class="line">Host: auth.example.com</span><br><span class="line">Authorization: Basic &lt;basic_auth_token&gt;               <span class="comment"># 1</span></span><br><span class="line">Content-Type: application/x-www-form-urlencoded</span><br><span class="line"></span><br><span class="line">grant_type=authorization_code                         <span class="comment"># 2</span></span><br><span class="line">    &amp;code=&lt;code&gt;                                      <span class="comment"># 3</span></span><br><span class="line">    &amp;redirect_uri=https://client.example.com/callback <span class="comment"># 4</span></span><br><span class="line">    &amp;code_verifier=&lt;code_verifier&gt;                    <span class="comment"># 5</span></span><br></pre></td></tr></table></figure></div>

<ol>
<li>(必須) クライアント ID とクライアントシークレットを使用して Basic 認証します</li>
<li>(必須) <code>grant_type=authorization_code</code> を指定します</li>
<li>(必須) 手順 8 認可レスポンスにより取得した <code>code</code> を指定します</li>
<li>(必須) クライアントが認可レスポンスを受け取るための URI を指定します</li>
<li>(認可リクエストで <code>code_challenge</code>, <code>code_challenge_method</code> を指定した場合必須) クライアントが生成した <code>code_verifier</code> を指定します</li>
</ol>
<p>トークンレスポンスは次の通りです。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">HTTP/1.1 200 OK</span><br><span class="line">Content-Type: application/json</span><br><span class="line"></span><br><span class="line">&#123;</span><br><span class="line">  <span class="string">&quot;access_token&quot;</span>: &lt;access_token&gt;,  <span class="comment"># 1</span></span><br><span class="line">  <span class="string">&quot;token_type&quot;</span>: <span class="string">&quot;bearer&quot;</span>,          <span class="comment"># 2</span></span><br><span class="line">  <span class="string">&quot;expires_in&quot;</span>: 3600,              <span class="comment"># 3</span></span><br><span class="line">  <span class="string">&quot;refresh_token&quot;</span>: &lt;refresh_token&gt; <span class="comment"># 4</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>

<ol>
<li>発行されたアクセストークン</li>
<li>トークンの種類</li>
<li>トークンの有効期限 (秒数)</li>
<li>リフレッシュトークン (本稿では触れない)</li>
</ol>
<h2 id="Google-Photos-API-を使ってみる">Google Photos API を使ってみる</h2><p>ここからは、実際に Google Photos API を利用するクライアントアプリケーションを実装し、OAuth の処理フローを確認していきます。</p>
<h3 id="GCP-マネジメントコンソールにてクライアントを登録する">GCP マネジメントコンソールにてクライアントを登録する</h3><p>GCP に実験用のプロジェクトを作成します。</p>
<img src="/images/2022/20221012a/スクリーンショット_2022-10-02_23.02.52.png" alt="" width="1200" height="807" loading="lazy">

<p>API &amp; Services から、OAuth consent screen を設定していきます。</p>
<img src="/images/2022/20221012a/スクリーンショット_2022-10-02_23.04.06.png" alt="" width="1200" height="807" loading="lazy">

<p>External を選択し、CREATE を押下します。</p>
<img src="/images/2022/20221012a/スクリーンショット_2022-10-02_23.04.18.png" alt="" width="1200" height="807" loading="lazy">

<p>App information として、App name に任意の名前を設定し、User support email にはご自身の Google アカウントのメールアドレスを設定します。</p>
<img src="/images/2022/20221012a/スクリーンショット_2022-10-02_23.04.54.png" alt="" width="1200" height="807" loading="lazy">

<p>Developer contact information として、ご自身の Google アカウントのメールアドレスを設定し、SAVE AND CONTINUE を押下します。</p>
<img src="/images/2022/20221012a/スクリーンショット_2022-10-02_23.05.11.png" alt="" width="1200" height="807" loading="lazy">

<p>Scopes 画面において、ADD OR REMOVE SCOPES を押下します。</p>
<img src="/images/2022/20221012a/スクリーンショット_2022-10-02_23.05.35.png" alt="" width="1200" height="807" loading="lazy">

<p>今回は Google Photos API を利用したいため、Manually add scopes に <code>https://www.googleapis.com/auth/photoslibrary.readonly</code> と入力し、ADD TO TABLE を押下します。なお、このスコープは、対象ユーザーの Google Photos リソースに対する読み取り権限を付与します。</p>
<img src="/images/2022/20221012a/スクリーンショット_2022-10-02_23.08.04.png" alt="" width="1200" height="807" loading="lazy">

<p>テーブルに Google Photos API のスコープが追加されるので、チェックボックスをチェックし、UPDATE を押下します。次いで、SAVE AND CONTINUE を押下します。</p>
<img src="/images/2022/20221012a/スクリーンショット_2022-10-02_23.08.15.png" alt="" width="1200" height="807" loading="lazy">

<p>Test users 画面においてテストのためのユーザーを設定します。ADD USERS を押下し、ご自身の Google アカウントのメールアドレスを入力の上、ADD を押下してください。</p>
<img src="/images/2022/20221012a/スクリーンショット_2022-10-02_23.09.02.png" alt="" width="1200" height="807" loading="lazy">
<img src="/images/2022/20221012a/スクリーンショット_2022-10-02_23.09.24.png" alt="" width="1200" height="807" loading="lazy">

<p>テーブルにご自身のメールアドレスが追加されていることを確認し、SAVE AND CONTINUE を押下します。</p>
<img src="/images/2022/20221012a/スクリーンショット_2022-10-02_23.09.29.png" alt="" width="1200" height="807" loading="lazy">

<p>Summary 画面において、各種パラメータが正しいことを確認の上、BACK TO DASHBOARD を押下します。</p>
<img src="/images/2022/20221012a/スクリーンショット_2022-10-02_23.09.43.png" alt="" width="1200" height="807" loading="lazy">
<img src="/images/2022/20221012a/スクリーンショット_2022-10-02_23.09.46.png" alt="" width="1200" height="807" loading="lazy">

<p>次に OAuth client ID を作成します。Credential 画面に遷移し、CREATE CREDENTIAL, OAuth client ID と順に押下します。</p>
<img src="/images/2022/20221012a/スクリーンショット_2022-10-02_23.10.30.png" alt="" width="1200" height="807" loading="lazy">

<p>実験用のサーバーは <code>localhost:8080</code> に立てる予定なので、Authorization redirect URI として <code>http://localhost:8080/callback</code> を入力し、CREATE を押下します。</p>
<img src="/images/2022/20221012a/スクリーンショット_2022-10-02_23.11.17.png" alt="" width="1200" height="807" loading="lazy">

<p>OAuth クライアントのクライアント ID とクライアントシークレットが作成されました。これらを控えておいてください。</p>
<img src="/images/2022/20221012a/スクリーンショット_2022-10-02_23.11.35.png" alt="" width="1200" height="807" loading="lazy">

<p>次に、今回利用する Photos Library API を有効にしておきます。Google Photos Library API のページにアクセスし、ENABLE を押下してください。</p>
<img src="/images/2022/20221012a/スクリーンショット_2022-10-02_23.32.14.png" alt="" width="1200" height="807" loading="lazy">

<p>ここまでで GCP の作業は完了です。次に、ローカル環境でアプリケーションを立て、ブラウザからアクセスします。</p>
<h3 id="クライアントを実装する">クライアントを実装する</h3><p>ここからは、Go によりクライアントアプリケーションを実装し、実際にローカル環境で動かしていきます。なお、テストに使用したソースコードは https://github.com/tmsick/tech-blog-oauth にアップロードしました。本稿にも <code>main.go</code> の全量を掲載します。</p>
<p>なお、このアプリケーションは次の環境変数を必要とします。これらは、先ほどの手順で Google から発行されたクライアント ID とクライアントシークレットです。</p>
<ul>
<li><code>GOOGLE_CLIENT_ID</code></li>
<li><code>GOOGLE_CLIENT_SECRET</code></li>
</ul>
<p>掲載しているコードは簡単な検証用のコードであり、適切なエラーハンドリングを省略していますのでご承知おきください。</p>
<div class="code-block"><figure class="highlight go"><input type="checkbox" id="code-wrap-172paf6-3" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-172paf6-3" title="コードの折り返しを切り替える"></label><figcaption><span>main.go</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">	<span class="string">&quot;crypto/rand&quot;</span></span><br><span class="line">	<span class="string">&quot;crypto/sha256&quot;</span></span><br><span class="line">	<span class="string">&quot;encoding/base64&quot;</span></span><br><span class="line">	<span class="string">&quot;encoding/json&quot;</span></span><br><span class="line">	<span class="string">&quot;fmt&quot;</span></span><br><span class="line">	<span class="string">&quot;io&quot;</span></span><br><span class="line">	<span class="string">&quot;log&quot;</span></span><br><span class="line">	<span class="string">&quot;math/big&quot;</span></span><br><span class="line">	<span class="string">&quot;net/http&quot;</span></span><br><span class="line">	<span class="string">&quot;net/url&quot;</span></span><br><span class="line">	<span class="string">&quot;os&quot;</span></span><br><span class="line">	<span class="string">&quot;strings&quot;</span></span><br><span class="line">	<span class="string">&quot;text/template&quot;</span></span><br><span class="line"></span><br><span class="line">	<span class="string">&quot;github.com/gorilla/sessions&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> TokenResponse <span class="keyword">struct</span> &#123;</span><br><span class="line">	AccessToken <span class="type">string</span> <span class="string">`json:&quot;access_token&quot;`</span></span><br><span class="line">	ExpiresIn   <span class="type">int</span>    <span class="string">`json:&quot;expires_in&quot;`</span></span><br><span class="line">	IDToken     <span class="type">string</span> <span class="string">`json:&quot;id_token&quot;`</span></span><br><span class="line">	Scope       <span class="type">string</span> <span class="string">`json:&quot;scope&quot;`</span></span><br><span class="line">	TokenType   <span class="type">string</span> <span class="string">`json:&quot;token_type&quot;`</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> Photos <span class="keyword">struct</span> &#123;</span><br><span class="line">	MediaItems []<span class="keyword">struct</span> &#123;</span><br><span class="line">		ID         <span class="type">string</span> <span class="string">`json:&quot;id&quot;`</span></span><br><span class="line">		ProductURL <span class="type">string</span> <span class="string">`json:&quot;productUrl&quot;`</span></span><br><span class="line">		BaseURL    <span class="type">string</span> <span class="string">`json:&quot;baseUrl&quot;`</span></span><br><span class="line">		MimeType   <span class="type">string</span> <span class="string">`json:&quot;mimeType&quot;`</span></span><br><span class="line">		Filename   <span class="type">string</span> <span class="string">`json:&quot;filename&quot;`</span></span><br><span class="line">	&#125; <span class="string">`json:&quot;mediaItems&quot;`</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">const</span> (</span><br><span class="line">	lenState        = <span class="number">30</span></span><br><span class="line">	lenCodeVerifier = <span class="number">64</span></span><br><span class="line">	redirectURI     = <span class="string">&quot;http://localhost:8080/callback&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> (</span><br><span class="line">	googleClientID     <span class="type">string</span></span><br><span class="line">	googleClientSecret <span class="type">string</span></span><br><span class="line">	store              = sessions.NewCookieStore([]<span class="type">byte</span>(os.Getenv(<span class="string">&quot;SESSION_KEY&quot;</span>)))</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">init</span><span class="params">()</span></span> &#123;</span><br><span class="line">	googleClientID = os.Getenv(<span class="string">&quot;GOOGLE_CLIENT_ID&quot;</span>)</span><br><span class="line">	googleClientSecret = os.Getenv(<span class="string">&quot;GOOGLE_CLIENT_SECRET&quot;</span>)</span><br><span class="line"></span><br><span class="line">	<span class="keyword">if</span> googleClientID == <span class="string">&quot;&quot;</span> &#123;</span><br><span class="line">		log.Fatal(<span class="string">&quot;Env var GOOGLE_CLIENT_ID is required&quot;</span>)</span><br><span class="line">	&#125;</span><br><span class="line">	<span class="keyword">if</span> googleClientSecret == <span class="string">&quot;&quot;</span> &#123;</span><br><span class="line">		log.Fatal(<span class="string">&quot;Env var GOOGLE_CLIENT_SECRET is required&quot;</span>)</span><br><span class="line">	&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">	http.HandleFunc(<span class="string">&quot;/&quot;</span>, handleIndex)</span><br><span class="line">	http.HandleFunc(<span class="string">&quot;/oauth&quot;</span>, handleOAuth)</span><br><span class="line">	http.HandleFunc(<span class="string">&quot;/callback&quot;</span>, handleCallback)</span><br><span class="line">	http.HandleFunc(<span class="string">&quot;/photos&quot;</span>, handlePhotos)</span><br><span class="line">	log.Print(<span class="string">&quot;Serving web server at localhost:8080&quot;</span>)</span><br><span class="line">	http.ListenAndServe(<span class="string">&quot;0.0.0.0:8080&quot;</span>, <span class="literal">nil</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">handleIndex</span><span class="params">(w http.ResponseWriter, r *http.Request)</span></span> &#123;</span><br><span class="line">	tpl, _ := template.ParseFiles(<span class="string">&quot;templates/index.html&quot;</span>)</span><br><span class="line">	tpl.Execute(w, <span class="literal">nil</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">handleOAuth</span><span class="params">(w http.ResponseWriter, r *http.Request)</span></span> &#123;</span><br><span class="line">	<span class="comment">// Save `state` and `code_verifier` to session</span></span><br><span class="line">	session, _ := store.Get(r, <span class="string">&quot;session&quot;</span>)</span><br><span class="line">	<span class="comment">// Generate a random state</span></span><br><span class="line">	state, _ := randomString(lenState)</span><br><span class="line">	session.Values[<span class="string">&quot;state&quot;</span>] = state</span><br><span class="line">	<span class="comment">// Generate code_verifier</span></span><br><span class="line">	codeVerifier, _ := randomString(lenCodeVerifier)</span><br><span class="line">	session.Values[<span class="string">&quot;code_verifier&quot;</span>] = codeVerifier</span><br><span class="line">	session.Save(r, w)</span><br><span class="line"></span><br><span class="line">	<span class="comment">// Generate code_challenge</span></span><br><span class="line">	b := sha256.Sum256([]<span class="type">byte</span>(codeVerifier))</span><br><span class="line">	codeChallenge := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(b[:])</span><br><span class="line"></span><br><span class="line">	<span class="comment">// Redirect the user agent == Make a authorization request</span></span><br><span class="line">	u, _ := url.Parse(<span class="string">&quot;https://accounts.google.com/o/oauth2/v2/auth&quot;</span>)</span><br><span class="line">	q := u.Query()</span><br><span class="line">	q.Add(<span class="string">&quot;response_type&quot;</span>, <span class="string">&quot;code&quot;</span>)                                           <span class="comment">// Indicate authorization code grant</span></span><br><span class="line">	q.Add(<span class="string">&quot;client_id&quot;</span>, googleClientID)                                       <span class="comment">// The client ID issued by Google</span></span><br><span class="line">	q.Add(<span class="string">&quot;state&quot;</span>, state)                                                    <span class="comment">// The random state</span></span><br><span class="line">	q.Add(<span class="string">&quot;scope&quot;</span>, <span class="string">&quot;https://www.googleapis.com/auth/photoslibrary.readonly&quot;</span>) <span class="comment">// The scope we need</span></span><br><span class="line">	q.Add(<span class="string">&quot;redirect_uri&quot;</span>, redirectURI)                                       <span class="comment">// The redirect URI</span></span><br><span class="line">	q.Add(<span class="string">&quot;code_challenge&quot;</span>, codeChallenge)                                   <span class="comment">// Code challenge</span></span><br><span class="line">	q.Add(<span class="string">&quot;code_challenge_method&quot;</span>, <span class="string">&quot;S256&quot;</span>)                                   <span class="comment">// Code challenge method</span></span><br><span class="line">	u.RawQuery = q.Encode()</span><br><span class="line">	http.Redirect(w, r, u.String(), http.StatusFound)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">handleCallback</span><span class="params">(w http.ResponseWriter, r *http.Request)</span></span> &#123;</span><br><span class="line">	<span class="comment">// Confirm `state` matches</span></span><br><span class="line">	session, _ := store.Get(r, <span class="string">&quot;session&quot;</span>)</span><br><span class="line">	<span class="keyword">if</span> r.URL.Query().Get(<span class="string">&quot;state&quot;</span>) != session.Values[<span class="string">&quot;state&quot;</span>] &#123;</span><br><span class="line">		w.WriteHeader(http.StatusInternalServerError)</span><br><span class="line">		fmt.Fprintln(w, <span class="string">&quot;Invalid state&quot;</span>)</span><br><span class="line">		<span class="keyword">return</span></span><br><span class="line">	&#125;</span><br><span class="line">	<span class="comment">// Get `code_verifier`</span></span><br><span class="line">	codeVerifier := session.Values[<span class="string">&quot;code_verifier&quot;</span>].(<span class="type">string</span>)</span><br><span class="line">	<span class="comment">// Clear session</span></span><br><span class="line">	session.Values[<span class="string">&quot;state&quot;</span>] = <span class="string">&quot;&quot;</span></span><br><span class="line">	session.Values[<span class="string">&quot;code_verifier&quot;</span>] = <span class="string">&quot;&quot;</span></span><br><span class="line">	session.Save(r, w)</span><br><span class="line"></span><br><span class="line">	<span class="comment">// Make a token request</span></span><br><span class="line">	code := r.URL.Query().Get(<span class="string">&quot;code&quot;</span>)</span><br><span class="line">	q := url.Values&#123;&#125;</span><br><span class="line">	q.Add(<span class="string">&quot;grant_type&quot;</span>, <span class="string">&quot;authorization_code&quot;</span>) <span class="comment">// Indicate token request</span></span><br><span class="line">	q.Add(<span class="string">&quot;code&quot;</span>, code)                       <span class="comment">// The authorization code</span></span><br><span class="line">	q.Add(<span class="string">&quot;redirect_uri&quot;</span>, redirectURI)        <span class="comment">// The redirect URI</span></span><br><span class="line">	q.Add(<span class="string">&quot;code_verifier&quot;</span>, codeVerifier)      <span class="comment">// Code verifier</span></span><br><span class="line">	req, _ := http.NewRequest(http.MethodPost, <span class="string">&quot;https://oauth2.googleapis.com/token&quot;</span>, strings.NewReader(q.Encode()))</span><br><span class="line">	req.SetBasicAuth(googleClientID, googleClientSecret)</span><br><span class="line">	req.Header.Add(<span class="string">&quot;Content-Type&quot;</span>, <span class="string">&quot;application/x-www-form-urlencoded&quot;</span>)</span><br><span class="line">	resp, _ := http.DefaultClient.Do(req)</span><br><span class="line">	<span class="keyword">defer</span> resp.Body.Close()</span><br><span class="line"></span><br><span class="line">	<span class="comment">// Capture the access token we&#x27;ve received</span></span><br><span class="line">	body, _ := io.ReadAll(resp.Body)</span><br><span class="line">	<span class="keyword">var</span> token TokenResponse</span><br><span class="line">	json.Unmarshal(body, &amp;token)</span><br><span class="line">	session.Values[<span class="string">&quot;access_token&quot;</span>] = token.AccessToken</span><br><span class="line">	session.Save(r, w)</span><br><span class="line"></span><br><span class="line">	<span class="comment">// Redirect the user agent to the photos page</span></span><br><span class="line">	http.Redirect(w, r, <span class="string">&quot;http://localhost:8080/photos&quot;</span>, http.StatusFound)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">handlePhotos</span><span class="params">(w http.ResponseWriter, r *http.Request)</span></span> &#123;</span><br><span class="line">	<span class="comment">// Fetch photos from Google Photos Library API using the access_token</span></span><br><span class="line">	session, _ := store.Get(r, <span class="string">&quot;session&quot;</span>)</span><br><span class="line">	accessToken := session.Values[<span class="string">&quot;access_token&quot;</span>].(<span class="type">string</span>)</span><br><span class="line">	req, _ := http.NewRequest(http.MethodGet, <span class="string">&quot;https://photoslibrary.googleapis.com/v1/mediaItems&quot;</span>, <span class="literal">nil</span>)</span><br><span class="line">	req.Header.Add(<span class="string">&quot;Authorization&quot;</span>, <span class="string">&quot;Bearer &quot;</span>+accessToken)</span><br><span class="line">	resp, _ := http.DefaultClient.Do(req)</span><br><span class="line">	<span class="keyword">defer</span> resp.Body.Close()</span><br><span class="line">	body, _ := io.ReadAll(resp.Body)</span><br><span class="line">	<span class="keyword">var</span> photos Photos</span><br><span class="line">	json.Unmarshal(body, &amp;photos)</span><br><span class="line"></span><br><span class="line">	<span class="comment">// Render photos</span></span><br><span class="line">	tpl, _ := template.ParseFiles(<span class="string">&quot;templates/photos.html&quot;</span>)</span><br><span class="line">	tpl.Execute(w, photos)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// randomString generates a secure random string of length `length`.</span></span><br><span class="line"><span class="comment">// It returns an error when `length` is negative or failed to use the platform&#x27;s</span></span><br><span class="line"><span class="comment">// secure pseudorandom number generator.</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">randomString</span><span class="params">(length <span class="type">int</span>)</span></span> (<span class="type">string</span>, <span class="type">error</span>) &#123;</span><br><span class="line">	<span class="keyword">if</span> length &lt; <span class="number">0</span> &#123;</span><br><span class="line">		<span class="keyword">return</span> <span class="string">&quot;&quot;</span>, fmt.Errorf(<span class="string">&quot;cannot generate random string of negative length %d&quot;</span>, length)</span><br><span class="line">	&#125;</span><br><span class="line">	<span class="keyword">var</span> s strings.Builder</span><br><span class="line">	<span class="keyword">for</span> s.Len() &lt; length &#123;</span><br><span class="line">		r, err := rand.Int(rand.Reader, big.NewInt(<span class="number">1</span>&lt;&lt;<span class="number">60</span>))</span><br><span class="line">		<span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">			<span class="keyword">return</span> <span class="string">&quot;&quot;</span>, err</span><br><span class="line">		&#125;</span><br><span class="line">		<span class="comment">// 1&lt;&lt;60 == 2**60 equals to 1,000,000,000,000,000 in hex.</span></span><br><span class="line">		s.WriteString(fmt.Sprintf(<span class="string">&quot;%015x&quot;</span>, r))</span><br><span class="line">	&#125;</span><br><span class="line">	<span class="keyword">return</span> s.String()[:length], <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div>

<p>アプリケーションのエンドポイントは次の 4 つです。</p>
<div class="scroll"><table>
<thead>
<tr>
<th>パス</th>
<th>説明</th>
</tr>
</thead>
<tbody><tr>
<td><code>/</code></td>
<td>アプリケーションのインデックス。「Fetch photos from Google Photos」ボタンが設置されている。</td>
</tr>
<tr>
<td><code>/oauth</code></td>
<td>OAuth 開始エンドポイント。ブラウザを Google Photos API の認可エンドポイントにリダイレクトする。</td>
</tr>
<tr>
<td><code>/callback</code></td>
<td>リダイレクトエンドポイント。認可レスポンスにより呼び出される。</td>
</tr>
<tr>
<td><code>/photos</code></td>
<td>認可完了後にブラウザがリダイレクトされるエンドポイント。Google Photos の画像が表示される。</td>
</tr>
</tbody></table></div>
<p>以下、ハンドラごとに処理を説明していきます。</p>
<ol>
<li><code>handleIndex()</code></li>
<li><code>handleOAuth()</code></li>
<li><code>handleCallback()</code></li>
<li><code>handlePhotos()</code></li>
</ol>
<h4 id="1-handleIndex">1. <code>handleIndex()</code></h4><p><code>handleIndex()</code> は、静的な HTML をレスポンスしているだけです。</p>
<h4 id="2-handleOAuth">2. <code>handleOAuth()</code></h4><p>78…86 行目において、ランダムな <code>state</code> と <code>codeVerifier</code> を生成し、ブラウザのセッションに保存しています。</p>
<p>88…90 行目において、<code>codeVerifier</code> から <code>codeChallenge</code> を生成しています。</p>
<p>92…103 行目において、認可リクエストの URL を生成し、ブラウザをリダイレクトしています。</p>
<h4 id="3-handleCallback">3. <code>handleCallback()</code></h4><p>107…113 行目において、ブラウザが保持している <code>state</code> と <code>code_verifier</code> を取得し、セッションをクリアしています。</p>
<p>115…120 行目において、ブラウザが保持していた <code>session</code> と認可サーバからレスポンスされた <code>state</code> が等しいことを確認しています。これにより、CSRF 攻撃を防いでいます。</p>
<p>122…133 行目において、トークンリクエストを生成し、リクエストをおこなっています。</p>
<p>135…140 行目において、トークンレスポンスからアクセストークンを取得し、ブラウザのセッションに保存しています。</p>
<p>142…143 行目において、ブラウザを <code>/photos</code> にリダイレクトしています。</p>
<h4 id="4-handlePhotos">4. <code>handlePhotos()</code></h4><p>147…156 行目において、セッションに保存されたアクセストークンを用いて Google Photos API にアクセスし、画像情報を取得しています。</p>
<p>158…160 行目において、画像情報 (URL) を含むレスポンスを返却しています。</p>
<img src="/images/2022/20221012a/スクリーンショット_2022-10-03_0.18.47.png" alt="" width="1200" height="807" loading="lazy">
<img src="/images/2022/20221012a/スクリーンショット_2022-10-03_0.18.57.png" alt="" width="1200" height="807" loading="lazy">
<img src="/images/2022/20221012a/スクリーンショット_2022-10-03_0.19.03.png" alt="" width="1200" height="807" loading="lazy">
<img src="/images/2022/20221012a/スクリーンショット_2022-10-03_0.20.59.png" alt="" width="1200" height="807" loading="lazy">
<img src="/images/2022/20221012a/スクリーンショット_2022-10-03_0.21.05.png" alt="" width="1200" height="807" loading="lazy">
<img src="/images/2022/20221012a/スクリーンショット_2022-10-03_0.21.12.png" alt="" width="1200" height="807" loading="lazy">

<h2 id="おわりに">おわりに</h2><p>OAuth, OpenID 周りには、なんとなく苦手意識があったのですが、今回仕様に基づいてスクラッチで実装することで、技術の基盤とセキュリティ上の考慮事項などを知ることができました。</p>
<p>IDaaS の採用が当然となった現在において、認証・認可周りの知識は、ウェブ技術者の基礎教養といえると思います。今後も引き続き認証・認可周りの情報をウォッチしていきます。</p>
<div id="footnotes"><hr><div id="footnotelist"><ol style="list-style:none; padding-left: 0;"><li id="fn:1"><span style="vertical-align: top; padding-right: 10px;">1.</span><span style="vertical-align: top;">仕様上、302 Found 以外の方法でユーザーエージェントをリダイレクトすることも許可されています。特に理由がなければ 302 Found でよいでしょう。</span> ↩</li></ol></div></div>]]></content>
    <summary type="html">業務で認証・認可に関する SaaS に触れる場面があり、そういえば OAuth, OpenID Connect の仕組みをちゃんと理解していなかったと思い、RFC を読みながら OAuth クライアントを実装してみました。</summary>
    <category term="認証認可" scheme="https://future-architect.github.io/categories/%E8%AA%8D%E8%A8%BC%E8%AA%8D%E5%8F%AF/"/>
    <category term="Go" scheme="https://future-architect.github.io/tags/Go/"/>
    <category term="OAuth" scheme="https://future-architect.github.io/tags/OAuth/"/>
    <category term="PKCE" scheme="https://future-architect.github.io/tags/PKCE/"/>
  </entry>
  <entry>
    <title>Auth0のトークン取得とITPへの対応</title>
    <link href="https://future-architect.github.io/articles/20221007a/"/>
    <id>https://future-architect.github.io/articles/20221007a/</id>
    <published>2022-10-06T15:00:00.000Z</published>
    <updated>2022-10-06T15:00:00.000Z</updated>
    <author><name>棚井龍之介</name></author>
    <content type="html"><![CDATA[<h2 id="はじめに">はじめに</h2><p>こんにちは。フューチャーの棚井龍之介と申します。認証認可連載の 4 本目を担当しました。</p>
<p>認証認可周りは最近触りたてでして、普段の開発業務では、Go・React・AWS を利用しています。</p>
<p>先日、React ベースのモバイル向け Web アプリに Auth0 の認証を実装したところ、Silent Authentication（サイレント認証）のタイミングでブラウザからトークンが消失し、ログイン状態が維持できない現象に遭遇しました。</p>
<p>調べたところ、Safari に搭載されているトラッキング防止機能の ITP（Intelligent Tracking Prevention &#x2F; インテリジェント・トラッキング・プリベンション）が原因だと判明しました。</p>
<p>調べる過程で、Cookie の基本的な機能からアドテク周りの技術要素、最近のプライバシー保護トレンドについて触れる機会を得ましたので、技術ブログとして整理しました。</p>
<p>本記事では、以下の内容を扱います。</p>
<ul>
<li>Cookie（1st Party、3rd Party）</li>
<li>ITP（Intelligent Tracking Prevention）</li>
<li>ITP による、Auth0 の PKCE フローへの影響<ul>
<li>対応方法</li>
</ul>
</li>
</ul>
<p>前半は Cookie とプライバシー保護の話で、後半が Auth0 と ITP の話です。</p>
<h2 id="Cookie-とは">Cookie とは</h2><p>Cookie とは、Web サイトに訪れたユーザの情報を、一時的に保存する機能のことです。<br>この「一時保存する機能」により、ユーザのログイン状態を維持することや、Web ページに再訪したときに「この人は、以前にウチのサイトに来た〇〇さんだ！」と判定できます。</p>
<img fetchpriority="high" src="/images/2022/20221007a/スクリーンショット_2022-10-05_3.12.54.png" alt="" width="1200" height="614">

<p>「以前に来た〇〇さん」と技術的に判定できると、次は「きっと〇〇さんならば…」という「Cookie により収集された個人属性の、Web 広告利用」へと直結させる動きが当然出てきます。</p>
<p>最近だとプライバシー保護の観点から、こういった Web 広告の規制が強化されるトレンドにあります。例えば、Web サイトのトップページに訪問した際に「Cookie の有効化に同意してください。Yes or No」がポップアップで表示されて、オプトインでの意思表示が求められるのは、このトレンドに乗ったものだと思います。</p>
<h3 id="Cookie-の分類">Cookie の分類</h3><p>続いて、Cookie の分類を見ていきます。</p>
<p>Cookie の発行元が、訪問しているサイト（ドメイン）と同じか別かにより、1st Party Cookie と 3rd Party Cookie と呼び方が変わります。</p>
<div class="scroll"><table>
<thead>
<tr>
<th>Cookie の発行元と訪問しているサイト(ドメイン)の関係</th>
<th>Cookie の呼び方</th>
</tr>
</thead>
<tbody><tr>
<td>同じ（Cookie の発行者は、訪問したサイトの運営者である）</td>
<td>1st Party Cookie</td>
</tr>
<tr>
<td>異なる（Cookie の発行者は、訪問しサイトとは別の外部の第三者である）</td>
<td>3rd Party Cookie</td>
</tr>
</tbody></table></div>
<p>主に、外部のサービスを利用して「ユーザ行動に合わせた Web 広告を表示したい（ターゲティング広告を実施したい）」や「ログイン状態管理などの特定の機能を外部に任せたい」ケースで 3rd Party Cookie が利用されることが多いと思います。</p>
<h2 id="ITP（Intelligent-Tracking-Prevention）とは">ITP（Intelligent Tracking Prevention）とは</h2><p>ひとこと言うと、ITP は「3rd Party Cookie の利用を禁止する技術」です。</p>
<p>先ほど、3rd Party Cookie により実現できることの 1 つに「ターゲティング広告（ユーザの行動履歴を元にした、Web 広告最適化）」があると述べました。ターゲティング広告により、各ユーザごとに個別最適化された（高いクリック率を獲得できる見込みの）Web 広告が表示できます。これにより、Web 広告のクリック率向上 → 購入率の向上が見込まれます。アドテク技術の進歩により、この Web 広告の精度が高まるほど、広告掲載ページ・広告を載せたい企業・その仕組みを提供する企業などの「Web マーケティング」界隈にとっては嬉しい世界になります。</p>
<p>その一方で、プライバシー保護の観点から、以下のような考え方が広まりつつあります。</p>
<p>（1）Web 広告の精度が高まる<br>↓<br>（2） ユーザの趣味嗜好を高精度で特定できる<br>↓<br>（3） 高精度な趣味嗜好の情報って、それはもはや個人情報では？<br>↓<br>（4） 個人情報を無断でクロスドメインに共有するは、プライバシー保護観点から NG では？<br>（例えば、あるユーザ α の行動履歴を取得した A 社が、その情報を Web 広告機能を提供する B 社に「α の同意がない状況で」共有するケース）</p>
<p>このような考え方はメガテック企業でも意識され、Safari や Chrome などのブラウザによる規制も進んでいます。</p>
<p>例えば、Apple では 2017 年から Safari での 3rd Party Cookie の利用制限を段階的に導入しており、iOS14 ではついに Safari だけでなく Chrome・Firefox 含めて 3rd Party Cookie の利用が全面禁止になりました。<br>Google においても、Chrome での 3rd Party Cookie の利用を「2024 年後半に向けて段階的に廃止する可能性がある」と発表し、その代替技術として「プライバシー・サンドボックス」の開発・ユーザテストを進めています。ただ、Google は当初は 2022 年 1 月に規制を開始すると発表していましたが、その後に 2023 年後半に開始予定と延期し、さらに 2024 年後半と再延期しています。</p>
<p>Safari での ITP 規制状況を見ると、ユーザー情報のクロスサイトトラッキングを規制しようとする Apple と、規制の穴を探すアドテク企業の戦いが垣間見られます。</p>
<div class="scroll"><table>
<thead>
<tr>
<th>アップデート</th>
<th>リリース</th>
<th>規制概要(*1)</th>
</tr>
</thead>
<tbody><tr>
<td>ITP1.0</td>
<td>2017 年 9 月</td>
<td>特定の 3rd Party Cookie は 24 時間で削除</td>
</tr>
<tr>
<td>ITP2.0</td>
<td>2018 年 9 月</td>
<td>特定の 3rd Party Cookie は即時削除</td>
</tr>
<tr>
<td>ITP2.1</td>
<td>2019 年 3 月</td>
<td>特定の 1st Party Cookie は 7 日間で削除</td>
</tr>
<tr>
<td>ITP2.2</td>
<td>2019 年 4 月</td>
<td>特定の 1st Party Cookie は 24 時間で削除</td>
</tr>
<tr>
<td>ITP2.3</td>
<td>2019 年 9 月</td>
<td>特定の localstrage 上のデータを即時削除</td>
</tr>
<tr>
<td>iOS13.1</td>
<td>2020 年 3 月</td>
<td>全ての 3rd Party Cookie を即時削除</td>
</tr>
<tr>
<td>iOS14.0</td>
<td>2020 年 9 月</td>
<td>iOS で稼働する Firefox, Chrome にも ITP を適用開始</td>
</tr>
</tbody></table></div>
<p>(*1)より詳細な規制内容はこちらを参照お願いします。</p>
<p>3rd Party Cookie は何もアドテク領域に閉じた技術ではなく、それ以外の分野でも利用されています。<br>次は、ITP により Auth0 のトークンがうまく動かなくなってしまう話に移ります。</p>
<h2 id="Auth0-のトークン取得フローと-ITP-ブロックへの対応">Auth0 のトークン取得フローと ITP ブロックへの対応</h2><p>Auth0 の React 用 SDK（auth0-react）を利用する場合、PKCE（Proof Key for Code Exchange）の認証認可フローに従います。フローの詳細はこちらのサイトに記載があります。</p>
<img src="/images/2022/20221007a/auth0_pkce.png" alt="auth0_pkceフロー" width="1200" height="976" loading="lazy">

<p>Auth0 の JavaScript SDK ではログイン成功後、トークンを取得して、ブラウザのインメモリにキャッシュされます（上図での （9） で取得した Access Token をインメモリに保存する）。インメモリに保存すると、ページ遷移や画面リロードの度にトークンの再取得が必要で色々と面倒になりそうですが、この辺のトークンリフレッシュを Auth0 側では「Silent Authentication（サイレント認証）」により解決しています。サイレント認証により、ログイン状態を維持しながら、トークンをインメモリでセキュアに保持できます。</p>
<h3 id="3rd-Party-Cookie-が-ITP-で強制消去される">3rd Party Cookie が ITP で強制消去される</h3><p>Auth0 x React でのログイン状態の管理が<br>（1）ID&#x2F;Pass を入力してログインし、トークンを取得<br>（2）サイレント認証でトークンのリフレッシュ、ログイン状態を維持<br>（3）トークンの破棄、ログアウト<br>の 3 つで完結するならばこれで話は終わりですが、昨今の ITP（3rd Party Cookie の禁止）により、（2） のサイレント認証に失敗するケースが出てきました。</p>
<p>例えば、会社 Z が Auth0 でログイン機能を実装した Web サービスを運営しているとします。サービスの運営会社 Z と Auth0 では「異なるドメインの会社」になるため、Auth0 の発行したトークンが、Z 社の Web ページ上では 3rd Party Cookie と判定されて、ブラウザにより Cookie が強制消去されるケースが有り得る、ということです。サイレント認証に失敗すると「ログイン状態を維持できない（ページ遷移・リロードの度にログイン処理を求めることになる）」ため、ユーザ体験を大きく損なってしまいます。</p>
<p>ITP により 3rd Party Cookie が消去されたとき、Auth0 のコンソール画面、Safari の Web Inspector それぞれで以下の挙動が得られます。</p>
<figure><img src="/images/2022/20221007a/failed_auth0.png" alt="auth0で失敗" width="1200" height="481" loading="lazy"><figcaption>Auth0 のコンソール画面では「ログインには成功」しているが「サイレント認証には失敗」している。</figcaption></figure>
<figure><img src="/images/2022/20221007a/failed_emulator.png" alt="サイレント認証に失敗" width="1200" height="212" loading="lazy"><figcaption>Web Inspector のネットワークのログから、前述した PKCE フロー （3） の <code>/authorize</code> は Call されているが、3rd Party Cookie がブラウザにより消去されているため、（7） の <code>/oauth/token</code> が Call されない。→ PKCE フローが途中終了しているため、認証は「失敗」である。</figcaption></figure>
<h3 id="ITP-への対応方法">ITP への対応方法</h3><p>サイレント認証が ITP により失敗する問題には、Auth0 公式が 2 つの対応方法を提示しています。</p>
<p>（1）リフレッシュトークンローテーションを設定する<br>（2）カスタムドメインを設定する。</p>
<p>この 2 つを設定すれば、ITP による 3rd Party Cookie 消失問題には対応できます。</p>
<p>また、ブラウザの設定変更で「ITP をオフにする」ことも可能なので、問題切り分け手法の 1 つとして覚えておくと、後々に役立つかも知れません。</p>
<p>以下は iPhone のブラウザ設定画面です。<br>スクショは ITP がオンの状態で撮影したものなので、Safari は「『サイト越えトラッキングを防ぐ』をオフ」にすることで、Chrome は「『Web サイト超えトラッキングを許可』をオン」にすることで、ITP を無効化できます。</p>
<img src="/images/2022/20221007a/スクリーンショット_2022-10-05_2.46.11.png" alt="" width="1200" height="985" loading="lazy">

<p>以下はリフレッシュトークン・カスタムドメインのどちらも設定せずに、エミュレーターの Safari 設定で ITP をオフにした場合の挙動です。Auth0 のコンソール画面、Web Inspector のログから、<code>/authorize</code>と<code>/oauth/token</code>が Call されて、PKCE フローが正常終了してサイレント認証に成功していることが分かります。</p>
<img src="/images/2022/20221007a/スクリーンショット_2022-10-05_2.57.58.png" alt="" width="1200" height="712" loading="lazy">

<h2 id="おわりに">おわりに</h2><p>モバイル向けの Web ページを作成してリリースし、リリース後の動作チェックで「あれ、iOS だとログイン状態が維持できてない！？」と気づきました。OAuth2.0 の仕様や PKCE の確認、インスペクタのログと認証フローと付き合わせながら、処理に失敗している場所の特定にまで至れました。（1）「ITP」について知見がなかったこと、（2） 事前チェックが Android と Chrome シミュレーターだけで、iOS は工数削減のため未実施だったことが根原因なのですが、スマホと PC・iOS と Android で色々細かいところが違うのなー（なので、モバイル実機テストは Android と iOS は両方やろう）と再認識する機会になりました。</p>
<h2 id="参考">参考</h2><ul>
<li>Webkit</li>
<li>Troubleshoot Renew Tokens When Using Safari</li>
<li>Auth0 の Silent Authentication (サイレント認証)と Refresh Token Rotation (リフレッシュトークンローテーション)を完全に理解した (い)</li>
</ul>
]]></content>
    <summary type="html">React ベースのモバイル向け Web アプリに Auth0 の認証を実装したところ、Silent Authentication（サイレント認証）のタイミングでブラウザからトークンが消失し、ログイン状態が維持できない現象に遭遇しました。調べる過程で、Cookie の基本的な機能からアドテク周りの技術要素、最近のプライバシー保護トレンドについて触れる機会を得ましたので...</summary>
    <category term="認証認可" scheme="https://future-architect.github.io/categories/%E8%AA%8D%E8%A8%BC%E8%AA%8D%E5%8F%AF/"/>
    <category term="Auth0" scheme="https://future-architect.github.io/tags/Auth0/"/>
    <category term="AuthN" scheme="https://future-architect.github.io/tags/AuthN/"/>
    <category term="PKCE" scheme="https://future-architect.github.io/tags/PKCE/"/>
  </entry>
  <entry>
    <title>Kong API Gatewayを使ってResource Serverを保護する</title>
    <link href="https://future-architect.github.io/articles/20221006a/"/>
    <id>https://future-architect.github.io/articles/20221006a/</id>
    <published>2022-10-05T15:00:00.000Z</published>
    <updated>2022-10-05T15:00:00.000Z</updated>
    <author><name>李光焄</name></author>
    <content type="html"><![CDATA[<p>こんにちは。TIGのLEEです。認証認可連載の3本目です。</p>
<p>前回はAWS API Gatewayを利用して、OIDC&#x2F;OAuth2.0におけるResource ServerをCustom Authorizerで保護する記事を書いてました。</p>
<p>https://future-architect.github.io/articles/20210610a/</p>
<p>今回はAPI Gatewayのミドルウェア製品となるKongを使ってResource Serverを構築する方法について話します。</p>
<h2 id="Kong-Gateway">Kong Gateway</h2><img fetchpriority="high" src="/images/2022/20221006a/gateway_overview.png" alt="gateway_overview.png" width="1200" height="507">

<p>KongはOSSから始まったAPIサーバのトラフィックを管理するためのミドルウェアです。</p>
<p>nginxベースにLuaJITエンジンを使ってLuaスクリプトが組み込めるWebプラットフォームのOpenRestyを採用し、Luaで書かれた様々なPlug-inをデフォルトで揃え、それを組み立てることでAPIGatewayの機能を実装しています。また、Luaスクリプトで新しいカスタムPlug-inを作りそれを組み込むことも可能です。</p>
<p>Enterprise版が登場してからはAPIGateway以外にも様々さサービスがありますが、今回はKong Gatewayにのみ注目して行きたいです。</p>
<h3 id="Kongの構造">Kongの構造</h3><p>構築の話になる前にかんたんにKongの構造を触れていきます。上の図のようにKongは基本的にConsumer&#x2F;Route&#x2F;Service&#x2F;LoadBalancer(Upstream)の4つのレイヤリングが存在します。Kongで使う様々なPlug-inはこの4つのレイヤーのどこか、もしくはGlobalに組み込むこともできます。</p>
<ul>
<li><strong>Consumer</strong>: Kongを実際利用するAPIClient(もしくはユーザ)を表すエンティティ</li>
<li><strong>Route</strong>: Requestのルールを定義するエンティティ</li>
<li><strong>Service</strong>: KongがProxyするBackendServiceを表すエンティティ</li>
<li><strong>Upstream</strong>: Backendの負荷分散やHealthCheckなどに使う仮想ホストのエンティティ</li>
</ul>
<h2 id="Actors">Actors</h2><p>構築にあたり、まずはOIDCの役者を揃えましょう。Front&#x2F;Backで分離された認証認可設計のためには、少なくとも下記3つのActorが必要になります。</p>
<img src="/images/2022/20221006a/kong-jwt.drawio.png" alt="kong-jwt.drawio.png" width="928" height="501" loading="lazy">

<h3 id="Keycloak-as-OpenID-Provider-Authorization-Server">Keycloak as OpenID Provider (Authorization Server)</h3><p>https://www.keycloak.org/getting-started/getting-started-docker</p>
<p>中心となる認可サーバはOSSのKeycloakを使いましょう。ID管理、トークンや証明書の発行&amp;管理、認証画面提供などの役割があります。<br>今回は上記リンク通り、Dockerを利用して構築します。Client設定は下記のVueの設定に従いましょう。<br>Keycloak構築はチュートリアル通りで問題ないので、詳細な実装方法は省略します。</p>
<h3 id="Vue-as-Relying-Party-Client">Vue as Relying Party (Client)</h3><p>https://www.keycloak.org/securing-apps/vue</p>
<p>FrontendとなるClientはVueを使います。認証後トークンの保持&amp;リフレッシュ、APIサーバへリクエストを送ったりします。<br>今回はVueを使いますが、keycloak-jsさえ組み込めば、どのFrameworkでもかんたんにRelyingPartyを作ることができます。<br>Keycloak同様リンク通り実装すれば問題ないので、詳細は省略します。</p>
<h3 id="Kong-as-Resource-Server-API-Server-Backend-Service">Kong as Resource Server (API Server, Backend Service)</h3><p>https://mockbin.org/</p>
<p>今回の保護対象となるResourceServerは、Gatewayとして前段に位置するKongと本丸となるBackend Service (API Server)に構成されます。Backend ServiceとしてはKongのチュートリアルで使われるMockbinをそのまま使います。</p>
<h2 id="Kongの構築">Kongの構築</h2><p>https://docs.konghq.com/gateway/latest/install/</p>
<p>まずはKongをインストールします。Dockerなど便利なオプションもあるので好きな方法でインストールしましょう。<br>Kongは設定の保存先としてDBを使うのでPostgreSQLもインストールが必要です。</p>
<p>Kongはデフォルトで…</p>
<ul>
<li>Port 8001：あらゆるエンティティ設定をするのAdminAPI</li>
<li>Port 8000：実際トラフィックをさばくProxy</li>
</ul>
<p>に分かれています。</p>
<h3 id="Service-Routing">Service &amp; Routing</h3><p>https://docs.konghq.com/gateway/latest/get-started/services-and-routes/</p>
<p>次にKongとBackendServiceとなるMockbinをつないで、KongのURLにアクセスするとMockbinのレスポンスが出るようにします。</p>
<div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-7mh4lz-1" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-7mh4lz-1" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">curl -i -s -X POST http://localhost:8001/services \</span><br><span class="line">  --data name=example_service \</span><br><span class="line">  --data url=<span class="string">&#x27;http://mockbin.org&#x27;</span></span><br><span class="line">curl -i -X POST http://localhost:8001/services/example_service/routes \</span><br><span class="line">  --data <span class="string">&#x27;paths[]=/mock&#x27;</span> \</span><br><span class="line">  --data name=example_route</span><br></pre></td></tr></table></figure></div>

<p>上記のように設定することでKongの<code>/mock</code>へのアクセスが、Mockbinの<code>/</code>にProxyされるようになります。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">curl -X GET http://localhost:8000/mock/requests</span><br></pre></td></tr></table></figure>

<p>そうすると上のようなCurlでMockbinのAPIパスである<code>/requests</code>のレスポンスが取得できます。</p>
<h2 id="APIを保護する">APIを保護する</h2><p>ここまで下準備が整ったところで、本題である認証機能実装に入ります。<br>ClientからのリクエストはBearerTokenとしてKeycloakが発行したJWTを乗せないと拒否するようにしたいので、トークンを検証するためにKong公式のJWTプラグインを使います。</p>
<div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-7mh4lz-2" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-7mh4lz-2" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">curl -X POST http://localhost:8001/plugins -d <span class="string">&quot;name=jwt&quot;</span></span><br><span class="line">curl -i http://localhost:8000/mock/requests <span class="comment"># 401 Unauthorized</span></span><br></pre></td></tr></table></figure></div>

<p>今回はJWTプラグインをGlobalに設定しますが、特定のServiceやRouteに限定して設定もできます。</p>
<h3 id="Consumer">Consumer</h3><p>次はConsumerの設定です。ConsumerはAPI Clientを表すエンティティですが、今回の場合は特定認可サーバ(Keycloak)に認証済みのユーザ全員を表すために予め設定するものになります。</p>
<div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-7mh4lz-3" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-7mh4lz-3" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">curl -X POST http://localhost:8001/consumers -d <span class="string">&quot;username=authorized_user&quot;</span></span><br></pre></td></tr></table></figure></div>

<h3 id="JWT-Credential">JWT Credential</h3><p>最後にConsumerにJWTを検証するための公開鍵を設定することで、「この検証されたトークンのBearerはこのConsumerで間違いない」ということを認証させるための設定をします。</p>
<div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-7mh4lz-4" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-7mh4lz-4" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">curl -X POST http://localhost:8001/consumers/authorized_user/jwt \</span><br><span class="line">-H <span class="string">&#x27;Content-Type: application/json&#x27;</span> \</span><br><span class="line">-d <span class="string">&#x27;&#123;&quot;key&quot;: &quot;http://localhost:8080/realms/&#123;REALM_NAME&#125;&quot;,</span></span><br><span class="line"><span class="string">     &quot;algorithm&quot;: &quot;RS256&quot;,</span></span><br><span class="line"><span class="string">     &quot;rsa_public_key&quot;: &quot;-----BEGIN PUBLIC KEY-----\nMIIBI...QIDAQAB\n-----END PUBLIC KEY-----&quot;&#125;&#x27;</span></span><br></pre></td></tr></table></figure></div>

<h4 id="key">key</h4><p>JWTのペイロード<code>iss</code>と同じ値を設定します。<br>このAPIにアクセスできるユーザ(<code>authorized_user</code>)は、みんな同じ認可サーバ(<code>Issuer</code>)から発行されたトークンを持ってる(<code>Bearer</code>)ことを意味します。</p>
<p>JWTプラグインのデフォルト設定で<code>config.key_claim_name=iss</code>となるので、別のClaimの値にしたい場合(例えば<code>aud</code>か<code>azp</code>など)はAdminAPIの<code>/plugins/&#123;jwt plug-in ID&#125;</code>をPATCHなどして変更も可能です。</p>
<h4 id="algorithm">algorithm</h4><p>Keycloakでデフォルトで発行するAccessToken(JWT)のアルゴリズムである<code>RS256</code>を指定します。<br>注意するところは、もしこの設定のリクエストで下記の<code>rsa_public_key</code>のPEM形式が正しくない場合でも、このフィールドのエラーメッセージが出ます。</p>
<h4 id="rsa-public-key">rsa_public_key</h4><p>Keycloakは同じRealmのユーザには同じ公開鍵でJWTを署名しているので、AdminConsoleの<code>Realm Settings &gt; Keys</code>から<code>RS256</code>の公開鍵をPEM形式でセットします。<br>一般的にRS256のJWT検証に使われるJWKs Endpointの証明書(<code>x5c</code>)と違い、公開鍵であることに注意しましょう。</p>
<h2 id="実際リクエストを送ってみる">実際リクエストを送ってみる</h2><p>普通アプリを作るならばここでClientであるVue上でKeycloakから取得したAccessToken(JWT)を<code>Authorization</code>ヘッダーに載せ、KongのAPIにアクセスするコードを書くことになります。<br>しかし、ここではKongの機能を確認するだけでいいので、Vueが保持するKeycloakのインスタンスをダンプさせAccessTokenを取得し、curlを使います。</p>
<img src="/images/2022/20221006a/スクリーンショット_2022-10-06_4.38.57.png" alt="スクリーンショット_2022-10-06_4.38.57.png" width="1200" height="707" loading="lazy">

<div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-7mh4lz-5" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-7mh4lz-5" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">curl http://localhost:8000/mock/requests -H <span class="string">&quot;Authorization: Bearer eyJhbGciOiJS...&quot;</span> | jq .</span><br><span class="line">&#123;</span><br><span class="line">    ...</span><br><span class="line">    <span class="string">&quot;headers&quot;</span>: &#123;</span><br><span class="line">        ...</span><br><span class="line">        <span class="string">&quot;authorization&quot;</span>: <span class="string">&quot;Bearer eyJhbGciOiJS...&quot;</span>,</span><br><span class="line">        <span class="string">&quot;x-consumer-username&quot;</span>: <span class="string">&quot;authorized_user&quot;</span>,</span><br><span class="line">        <span class="string">&quot;x-credential-identifier&quot;</span>: <span class="string">&quot;http://localhost:8080/realms/&#123;REALM_NAME&#125;&quot;</span>,</span><br><span class="line">        ...</span><br><span class="line">    &#125;,</span><br><span class="line">    ...</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div>

<p>そうするとMockbinが受け取ったHeaderを上記のようなレスポンスとして返してくれます。</p>
<h2 id="さいごに">さいごに</h2><p>といった感じで簡単に触ってみましたが、いかがだったでしょうか。</p>
<p>今回は割愛しましたが、<code>exp</code>Claimで有効期限のチェックもできますし、設定の<code>config.key_claim_name</code>とプラグインを適用するRoute&#x2F;Serviceを調整する機能を組み合わせることで認可機能の実装もできます。</p>
<p>個人的にはどのアカウントからのリクエストかわかるように、ペイロードの<code>sub</code>など一部のClaimを後ろにヘッダーとして流せる機能があったら良かったなとも思いましたが、例えばこういったカスタムプラグインを組み合わせることでなんとかなりそうです。</p>
]]></content>
    <summary type="html">API Gatewayのミドルウェア製品となるKongを使ってResource Serverを構築する方法について話します。</summary>
    <category term="認証認可" scheme="https://future-architect.github.io/categories/%E8%AA%8D%E8%A8%BC%E8%AA%8D%E5%8F%AF/"/>
    <category term="APIGateway" scheme="https://future-architect.github.io/tags/APIGateway/"/>
    <category term="JWT" scheme="https://future-architect.github.io/tags/JWT/"/>
    <category term="Keycloak" scheme="https://future-architect.github.io/tags/Keycloak/"/>
    <category term="OAuth" scheme="https://future-architect.github.io/tags/OAuth/"/>
    <category term="OIDC" scheme="https://future-architect.github.io/tags/OIDC/"/>
  </entry>
  <entry>
    <title>Casbinで始めるアクセス制御</title>
    <link href="https://future-architect.github.io/articles/20221004a/"/>
    <id>https://future-architect.github.io/articles/20221004a/</id>
    <published>2022-10-03T15:00:00.000Z</published>
    <updated>2022-10-03T15:00:00.000Z</updated>
    <author><name>真野隼記</name></author>
    <content type="html"><![CDATA[<h2 id="はじめに">はじめに</h2><p>TIG真野です。認証認可連載の2本目です。</p>
<p>認証認可がテーマの中で、アクセス制御と聞くとちょっと外れているかもと思いましたが、Casbinについて紹介します。</p>
<p>トップページにも <code>An authorization library that supports access control models like ACL...</code> とあり、アクセス制御を支援する認可ライブラリだよって書いているので、OKと判断しました。</p>
<h2 id="Casbinについて">Casbinについて</h2><p>ACL、RBAC、ABACなどの様々なモデルでアクセス制御を行えるライブラリです。</p>
<p>私が最初に存在を知ったのは、avelino&#x2F;awesome-go に載っていたことからだったので、てっきりGo言語のみのライブラリかと思っていました。</p>
<p>実際はドキュメントを見ると、複数の言語をサポートしています。Go以外にも、Java, Node.js, PHP, Python, .NET, C#, C++, Rust, Delphi, Lua, Dart, Elixirに対応しているとのこと（言語によっては一部の機能が使えないなどあるようです）。</p>
<p>ドキュメントに書いてることがシンプルだったのでそのまま転載、抜粋します。</p>
<p>Casbinが行うこと：</p>
<ol>
<li><code>&#123;subject, object, action&#125;</code> の形式や独自定義のカスタマイズされた形式のポリシーを適用します</li>
<li>アクセス制御モデルとそのポリシーの保存をハンドリングします</li>
<li>ロール・ユーザー間のマッピングとロール・ロール間のマッピング（RBACのロール階層管理）</li>
<li>root や administrator のようなスーパーユーザのサポート</li>
<li>ルールのマッチングをサポートする複数の組み込み演算子もサポートします。 例えば、 keyMatch はリソース キー <code>/foo/bar</code> をパターン <code>/foo*</code> のマッピング</li>
</ol>
<p>Casbinが行わないこと：</p>
<ol>
<li>認証 (ログイン時の ユーザー名 と パスワード の検証)</li>
<li>ユーザーまたはロールのリスト管理。 プロジェクトがこれらのエンティティを管理する方が利便性が高いと考えています。 Casbinはパスワードを保管しない</li>
</ol>
<p>仕組みとしては、 PERM メタモデル (Policy, Effect, Request, Matchers) にもとづいて動作するとのこと。</p>
<h2 id="ACL、RBAC、ABACについて">ACL、RBAC、ABACについて</h2><p>それぞれ用語だけ簡単に触れます。</p>
<ul>
<li>ACL(Access Controll List)<ul>
<li>アクセス制御</li>
</ul>
</li>
<li>RBAC(Role Based Access Control)<ul>
<li>ロールベースアクセス制御</li>
</ul>
</li>
<li>ABAC(Attribute Based Access Control)<ul>
<li>属性ベースアクセス制御</li>
</ul>
</li>
</ul>
<p>アクセス制御について詳しい解説は、次のようなサイトを見ると良いと思います。</p>
<ul>
<li>https://kenfdev.hateblo.jp/entry/2020/01/13/115032</li>
<li>https://ja.wikipedia.org/wiki/%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E5%88%B6%E5%BE%A1%E3%83%AA%E3%82%B9%E3%83%88</li>
<li>https://www.cloudflare.com/ja-jp/learning/access-management/role-based-access-control-rbac/</li>
<li>https://www.okta.com/jp/identity-101/role-based-access-control-vs-attribute-based-access-control/</li>
</ul>
<h2 id="PERM-メタモデル-について">PERM メタモデル について</h2><p>Casbin以外で聞いたことが無いですが（一般用語でしたらすいません）、Policy, Effect, Request, Matchersの略です。</p>
<img fetchpriority="high" src="/images/2022/20221004a/casbin_image.png" alt="casbin_image.png" width="1200" height="531">

<p>ファイルシステムのACLのイメージ図です。ポリシー定義がそのファイルの権限を誰が持っているかのリストです。モデル定義はそれをもとにどのように動作させるかを示しています。</p>
<p>図だと、以下を示しています。</p>
<ul>
<li>aliceはdata1を読み取りOK</li>
<li>bobはdata2を書き込みOK</li>
</ul>
<p>sub, obj, actは <code>だれ</code>が、<code>何</code>を、<code>どうする</code> に置き換えるとイメージしやすいと思います。</p>
<h2 id="触ってみる（Go）">触ってみる（Go）</h2><p>GoでどのようにCasbinを動かすのか、触ってみます。</p>
<p>最初にPolicyを定義します。実用的にはPostgreSQL&#x2F;MySQLといったDBやAmazon S3などに配備すると思います。そういったAdaptorも用意されています。今回は簡易的にCSVファイルを用います。</p>
<p>例としてRBACをイメージしています。</p>
<figure class="highlight csv"><figcaption><span>policy.csv</span></figcaption><table><tbody><tr><td class="code"><pre><span class="line"><span class="csv-col-1">p</span><span class="csv-comma">,</span><span class="csv-col-2"> admin</span><span class="csv-comma">,</span><span class="csv-col-3"> file1</span><span class="csv-comma">,</span><span class="csv-col-4"> read</span></span><br><span class="line"><span class="csv-col-1">p</span><span class="csv-comma">,</span><span class="csv-col-2"> admin</span><span class="csv-comma">,</span><span class="csv-col-3"> file2</span><span class="csv-comma">,</span><span class="csv-col-4"> read</span></span><br><span class="line"><span class="csv-col-1">p</span><span class="csv-comma">,</span><span class="csv-col-2"> admin</span><span class="csv-comma">,</span><span class="csv-col-3"> file3</span><span class="csv-comma">,</span><span class="csv-col-4"> read</span></span><br><span class="line"><span class="csv-col-1">p</span><span class="csv-comma">,</span><span class="csv-col-2"> Aさん</span><span class="csv-comma">,</span><span class="csv-col-3"> file4</span><span class="csv-comma">,</span><span class="csv-col-4"> read</span></span><br><span class="line"><span class="csv-col-1">g</span><span class="csv-comma">,</span><span class="csv-col-2"> Bさん</span><span class="csv-comma">,</span><span class="csv-col-3"> file5</span><span class="csv-comma">,</span><span class="csv-col-4"> read</span></span><br><span class="line"><span class="csv-col-1">g</span><span class="csv-comma">,</span><span class="csv-col-2"> Aさん</span><span class="csv-comma">,</span><span class="csv-col-3"> admin</span></span><br></pre></td></tr></tbody></table></figure>

<p>見たまんまですが、adminロールを持っている人はfile1~file3に対して権限があり、Aさんのみadminです。<br>また、Aさんはfile4, Bさんはfile5に権限を個別に持っています。</p>
<p>Goのコードとしては、モデルのロード、ポリシーのロードを行い生成する <code>casbin.NewEnforcer()</code> がメインどころです。このインスタンスを生成できるとあとは <code>Enforce()</code> で判定可能です。</p>
<div class="code-block"><figure class="highlight go"><input type="checkbox" id="code-wrap-9en2h8-1" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-9en2h8-1" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">	<span class="string">&quot;fmt&quot;</span></span><br><span class="line">	<span class="string">&quot;log&quot;</span></span><br><span class="line"></span><br><span class="line">	<span class="string">&quot;github.com/casbin/casbin/v2&quot;</span></span><br><span class="line">	<span class="string">&quot;github.com/casbin/casbin/v2/model&quot;</span></span><br><span class="line">	fileadapter <span class="string">&quot;github.com/casbin/casbin/v2/persist/file-adapter&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line"></span><br><span class="line">	modelParam, err := model.NewModelFromString(<span class="string">`</span></span><br><span class="line"><span class="string">[request_definition]</span></span><br><span class="line"><span class="string">r = sub, obj, act</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">[policy_definition]</span></span><br><span class="line"><span class="string">p = sub, obj, act</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">[role_definition]</span></span><br><span class="line"><span class="string">g = _ , _</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">[policy_effect]</span></span><br><span class="line"><span class="string">e = some(where (p.eft == allow))</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">[matchers]</span></span><br><span class="line"><span class="string">m = g(r.sub, p.sub) &amp;&amp; r.obj == p.obj &amp;&amp; r.act == p.act</span></span><br><span class="line"><span class="string">`</span>)</span><br><span class="line">	<span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">		log.Fatalf(<span class="string">&quot;NewModelFromString: %s&quot;</span>, err)</span><br><span class="line">	&#125;</span><br><span class="line"></span><br><span class="line">	enforcer, err := casbin.NewEnforcer(modelParam, fileadapter.NewAdapter(<span class="string">&quot;policy.csv&quot;</span>))</span><br><span class="line">	<span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">		log.Fatalf(<span class="string">&quot;NewEnforcer: %s&quot;</span>, err)</span><br><span class="line">	&#125;</span><br><span class="line"></span><br><span class="line">	in := [][]any&#123;</span><br><span class="line">		&#123;<span class="string">&quot;Aさん&quot;</span>, <span class="string">&quot;file1&quot;</span>, <span class="string">&quot;read&quot;</span>&#125;,</span><br><span class="line">		&#123;<span class="string">&quot;Aさん&quot;</span>, <span class="string">&quot;file1&quot;</span>, <span class="string">&quot;write&quot;</span>&#125;,</span><br><span class="line">		&#123;<span class="string">&quot;Aさん&quot;</span>, <span class="string">&quot;file4&quot;</span>, <span class="string">&quot;read&quot;</span>&#125;,</span><br><span class="line">		&#123;<span class="string">&quot;Bさん&quot;</span>, <span class="string">&quot;file1&quot;</span>, <span class="string">&quot;read&quot;</span>&#125;,</span><br><span class="line">	&#125;</span><br><span class="line"></span><br><span class="line">	<span class="keyword">for</span> _, v := <span class="keyword">range</span> in &#123;</span><br><span class="line">		ok, err := enforcer.Enforce(v...)</span><br><span class="line">		<span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">			log.Fatalf(<span class="string">&quot;enforce: %s&quot;</span>, err)</span><br><span class="line">		&#125;</span><br><span class="line">		fmt.Printf(<span class="string">&quot;%v: %v\n&quot;</span>, v, ok)</span><br><span class="line">	&#125;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div>

<p>これを動かすと、次のように想定通りの結果を得られます。</p>
<figure class="highlight plaintext"><figcaption><span>実行結果</span></figcaption><table><tr><td class="code"><pre><span class="line">[Aさん file1 read]: true</span><br><span class="line">[Aさん file1 write]: false</span><br><span class="line">[Aさん file4 read]: true</span><br><span class="line">[Bさん file1 read]: false</span><br></pre></td></tr></table></figure>

<p>CasbinのAPIの使い方で言えば、判定するための実装そのものより、モデルのmatcherの書き方や、これらの定義をどのようにロードさせたり、変更があった場合に追随させるかといったところの方が難しいと思います。</p>
<p>matcherの文法:</p>
<ul>
<li>https://casbin.io/ja/docs/function にかかれている通り、組み込み関数が使えます。<ul>
<li>ワイルドカード指定などもこれで対応できます</li>
</ul>
</li>
</ul>
<p>adaptor:</p>
<ul>
<li>https://casbin.io/docs/adapters に記載されている通り、複数のデータソースに対応しています。地味に<code>Ent</code>や<code>sqlx</code> といったO&#x2F;Rマッパーも対応していて細かいです<ul>
<li><code>AutoSave</code> ですが、enforcerは <code>UpdatePolicy()</code> で動的に定義を変更できるため、それを自動で保存する機能です</li>
<li>例えば、何か新しいファイルやレコードが追加された時に権限を更新→自動で永続化先まで反映してくれるといった具合です</li>
</ul>
</li>
</ul>
<h2 id="HTTPサーバの利用できるAPIをアクセス制御する">HTTPサーバの利用できるAPIをアクセス制御する</h2><img src="/images/2022/20221004a/casbin_server.drawio.png" alt="casbin_server.drawio.png" width="1200" height="486" loading="lazy">

<p>さきほどの例だとあまりイメージが付きにくいと思うので、Web APIの URL＋Method でアクセス制限する例を実装していみます。</p>
<p>ここでは説明のためスクラッチで書いていますが、Echo, Gin、Chiなどすでにミドルウェアで準備されています。</p>
<ul>
<li>https://casbin.io/ja/docs/middlewares</li>
</ul>
<p>今回は go-chi を用いて実装します。それっぽい例を探すのが大変だっため、_examples&#x2F;rest を流用しました。オリジナルのコードはそちらを参照ください。</p>
<p>まずはモデル定義です。</p>
<div class="code-block"><figure class="highlight plaintext"><input type="checkbox" id="code-wrap-9en2h8-2" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-9en2h8-2" title="コードの折り返しを切り替える"></label><figcaption><span>model.conf</span></figcaption><table><tr><td class="code"><pre><span class="line">[request_definition]</span><br><span class="line">r = sub, obj, act</span><br><span class="line"></span><br><span class="line">[policy_definition]</span><br><span class="line">p = sub, obj, act</span><br><span class="line"></span><br><span class="line">[policy_effect]</span><br><span class="line">e = some(where (p.eft == allow))</span><br><span class="line"></span><br><span class="line">[matchers]</span><br><span class="line">m = r.sub == p.sub &amp;&amp; keyMatch(r.obj, p.obj) &amp;&amp; (r.act == p.act || p.act == &quot;*&quot;)</span><br></pre></td></tr></table></figure></div>

<p>最後のmachersだけ、条件が増えています。<br><code>keyMatch</code> はワイルドカードを許容する関数です。例えば、 <code>/articles/*</code> で <code>/articles/1234</code> とか、 <code>/articles/1234/comments/5678</code> などを許容したいので利用しています。詳細はこちらを参照ください。<br>今回はワイルドカードで許容できるようにしたいので、 <code>act</code> 側はORで繋いでいます。 <code>keyMatch</code> だと GET, POST, DELETE などを <code>*</code> で許容できなかったのでこの書き方をしています。</p>
<p>ポリシーは今回もCSVファイルで定義します。</p>
<figure class="highlight csv"><figcaption><span>policy.csv</span></figcaption><table><tbody><tr><td class="code"><pre><span class="line"><span class="csv-col-1">p</span><span class="csv-comma">,</span><span class="csv-col-2"> guest</span><span class="csv-comma">,</span><span class="csv-col-3"> /</span><span class="csv-comma">,</span><span class="csv-col-4"> *</span></span><br><span class="line"><span class="csv-col-1">p</span><span class="csv-comma">,</span><span class="csv-col-2"> guest</span><span class="csv-comma">,</span><span class="csv-col-3"> /ping</span><span class="csv-comma">,</span><span class="csv-col-4"> *</span></span><br><span class="line"><span class="csv-col-1">p</span><span class="csv-comma">,</span><span class="csv-col-2"> member</span><span class="csv-comma">,</span><span class="csv-col-3"> /</span><span class="csv-comma">,</span><span class="csv-col-4"> GET</span></span><br><span class="line"><span class="csv-col-1">p</span><span class="csv-comma">,</span><span class="csv-col-2"> member</span><span class="csv-comma">,</span><span class="csv-col-3"> /ping</span><span class="csv-comma">,</span><span class="csv-col-4"> GET</span></span><br><span class="line"><span class="csv-col-1">p</span><span class="csv-comma">,</span><span class="csv-col-2"> member</span><span class="csv-comma">,</span><span class="csv-col-3"> /articles</span><span class="csv-comma">,</span><span class="csv-col-4"> GET</span></span><br><span class="line"><span class="csv-col-1">p</span><span class="csv-comma">,</span><span class="csv-col-2"> member</span><span class="csv-comma">,</span><span class="csv-col-3"> /articles/*</span><span class="csv-comma">,</span><span class="csv-col-4"> GET</span></span><br><span class="line"><span class="csv-col-1">p</span><span class="csv-comma">,</span><span class="csv-col-2"> owner</span><span class="csv-comma">,</span><span class="csv-col-3"> /</span><span class="csv-comma">,</span><span class="csv-col-4"> GET</span></span><br><span class="line"><span class="csv-col-1">p</span><span class="csv-comma">,</span><span class="csv-col-2"> owner</span><span class="csv-comma">,</span><span class="csv-col-3"> /ping</span><span class="csv-comma">,</span><span class="csv-col-4"> GET</span></span><br><span class="line"><span class="csv-col-1">p</span><span class="csv-comma">,</span><span class="csv-col-2"> owner</span><span class="csv-comma">,</span><span class="csv-col-3"> /articles</span><span class="csv-comma">,</span><span class="csv-col-4"> *</span></span><br><span class="line"><span class="csv-col-1">p</span><span class="csv-comma">,</span><span class="csv-col-2"> owner</span><span class="csv-comma">,</span><span class="csv-col-3"> /articles/*</span><span class="csv-comma">,</span><span class="csv-col-4"> *</span></span><br><span class="line"><span class="csv-col-1">p</span><span class="csv-comma">,</span><span class="csv-col-2"> admin</span><span class="csv-comma">,</span><span class="csv-col-3"> /*</span><span class="csv-comma">,</span><span class="csv-col-4"> *</span></span><br></pre></td></tr></tbody></table></figure>

<p>全てロールで、guest &lt; member &lt; owner &lt; admin の順番で権限が強くなるイメージです。</p>
<p>次に、Casbinの判定をchiのミドルウェアに設定する実装イメージを書いてみました。</p>
<div class="code-block"><figure class="highlight go"><input type="checkbox" id="code-wrap-9en2h8-3" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-9en2h8-3" title="コードの折り返しを切り替える"></label><figcaption><span>main.go</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">	flag.Parse()</span><br><span class="line">	r := chi.NewRouter()</span><br><span class="line">	r.Use(middleware.RequestID)</span><br><span class="line">	<span class="comment">// 中略</span></span><br><span class="line"></span><br><span class="line">	<span class="comment">// Casbinのモデル、ポリシーをロード</span></span><br><span class="line">	casbinModel, err := model.NewModelFromFile(<span class="string">&quot;model.conf&quot;</span>)</span><br><span class="line">	<span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">		log.Fatalf(<span class="string">&quot;NewModelFromFile: %s&quot;</span>, err)</span><br><span class="line">	&#125;</span><br><span class="line">	enforcer, err := casbin.NewEnforcer(casbinModel, fileadapter.NewAdapter(<span class="string">&quot;policy.csv&quot;</span>))</span><br><span class="line">	<span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">		log.Fatalf(<span class="string">&quot;NewEnforcer: %s&quot;</span>, err)</span><br><span class="line">	&#125;</span><br><span class="line"></span><br><span class="line">	<span class="comment">// ミドルウェアに設定</span></span><br><span class="line">	r.Use(CasbinAuthorizer(enforcer))</span><br></pre></td></tr></table></figure></div>

<p>続いてミドルウェア本体です。かなり端折って書いています。コードコメントにも書いていますが、本来はログイン後にセッションか何かにユーザーIDを載せ、紐づくユーザーロールをDBから取得するようなイメージでいます（あるいはJSTトークンにロールを載せてもらうかなど）。取得したロールも、本来は http.Requestの <code>context.Context</code> に載せて引き回した方が自然かもしれませんが、簡易的にリクエストヘッダーから取っています。</p>
<p>main関数側で設定した <code>casbin.Enforcer</code> で、リクエストを検証して、OKであれば後続へ。NGであれば403を返します。</p>
<div class="code-block"><figure class="highlight go"><input type="checkbox" id="code-wrap-9en2h8-4" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-9en2h8-4" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">CasbinAuthorizer</span><span class="params">(e *casbin.Enforcer)</span></span> <span class="function"><span class="keyword">func</span><span class="params">(next http.Handler)</span></span> http.Handler &#123;</span><br><span class="line">	<span class="keyword">return</span> <span class="function"><span class="keyword">func</span><span class="params">(next http.Handler)</span></span> http.Handler &#123;</span><br><span class="line">		fn := <span class="function"><span class="keyword">func</span><span class="params">(w http.ResponseWriter, r *http.Request)</span></span> &#123;</span><br><span class="line"></span><br><span class="line">			<span class="comment">// 何かしらミドルウェアの前処理（セッションやJWTトークンから取得）でロールがリクエストヘッダーに入っているものとする</span></span><br><span class="line">			role := r.Header.Get(<span class="string">&quot;user_role&quot;</span>)</span><br><span class="line">			<span class="keyword">if</span> role == <span class="string">&quot;&quot;</span> &#123;</span><br><span class="line">				role = <span class="string">&quot;guest&quot;</span></span><br><span class="line">			&#125;</span><br><span class="line"></span><br><span class="line">			<span class="comment">// casbinでリクエストを検証（ロール、URL、メソッド）</span></span><br><span class="line">			res, err := e.Enforce(role, r.URL.Path, r.Method)</span><br><span class="line">			<span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">				http.Error(w, err.Error(), http.StatusInternalServerError)</span><br><span class="line">				<span class="keyword">return</span></span><br><span class="line">			&#125;</span><br><span class="line">			<span class="keyword">if</span> res &#123;</span><br><span class="line">				next.ServeHTTP(w, r)</span><br><span class="line">			&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">				http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)</span><br><span class="line">				<span class="keyword">return</span></span><br><span class="line">			&#125;</span><br><span class="line">		&#125;</span><br><span class="line"></span><br><span class="line">		<span class="keyword">return</span> http.HandlerFunc(fn)</span><br><span class="line">	&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div>

<p>実際にcurlで試してみます。ロールをリクエストヘッダで切り替えていますが、本来はこれだと意味がないので、ログイン処理などを追加して、サーバ側でロールを判定するように改修し、クライアントがロールを指定できなくする必要があります。</p>
<div class="code-block"><figure class="highlight console"><input type="checkbox" id="code-wrap-9en2h8-5" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-9en2h8-5" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">guest ロール</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">curl  http://localhost:3333/ping</span></span><br><span class="line">pong</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">curl  http://localhost:3333/articles</span></span><br><span class="line">Forbidden</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">member ロール</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">curl -H <span class="string">&#x27;user_role:member&#x27;</span>  http://localhost:3333/articles</span></span><br><span class="line">[&#123;&quot;id&quot;:&quot;1&quot;,&quot;user_id&quot;:100,&quot;title&quot;:&quot;Hi&quot;,&quot;slug&quot;:&quot;hi&quot;,&quot;user&quot;:&#123;&quot;id&quot;:100,&quot;name&quot;:&quot;Peter&quot;,&quot;role&quot;:&quot;collaborator&quot;&#125;,&quot;elapsed&quot;:10&#125;,&#123;&quot;id&quot;:&quot;2&quot;,&quot;user_id&quot;:200,&quot;title&quot;:&quot;sup&quot;,&quot;slug&quot;:&quot;sup&quot;,&quot;user&quot;:&#123;&quot;id&quot;:200,&quot;name&quot;:&quot;Julia&quot;,&quot;role&quot;:&quot;collaborator&quot;&#125;,&quot;elapsed&quot;:10&#125;,&#123;&quot;id&quot;:&quot;3&quot;,&quot;u</span><br><span class="line">ser_id&quot;:300,&quot;title&quot;:&quot;alo&quot;,&quot;slug&quot;:&quot;alo&quot;,&quot;elapsed&quot;:10&#125;,&#123;&quot;id&quot;:&quot;4&quot;,&quot;user_id&quot;:400,&quot;title&quot;:&quot;bonjour&quot;,&quot;slug&quot;:&quot;bonjour&quot;,&quot;elapsed&quot;:10&#125;,&#123;&quot;id&quot;:&quot;5&quot;,&quot;user_id&quot;:500,&quot;title&quot;:&quot;whats up&quot;,&quot;slug&quot;:&quot;whats-up&quot;,&quot;elapsed&quot;:10&#125;]</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">curl -H <span class="string">&#x27;user_role:member&#x27;</span> -X DELETE http://localhost:3333/articles/1</span></span><br><span class="line">Forbidden</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">owner ロール</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">curl -H <span class="string">&#x27;user_role:owner&#x27;</span> -X DELETE http://localhost:3333/articles/1</span></span><br><span class="line">&#123;&quot;id&quot;:&quot;1&quot;,&quot;user_id&quot;:100,&quot;title&quot;:&quot;Hi&quot;,&quot;slug&quot;:&quot;hi&quot;,&quot;user&quot;:&#123;&quot;id&quot;:100,&quot;name&quot;:&quot;Peter&quot;,&quot;role&quot;:&quot;collaborator&quot;&#125;,&quot;elapsed&quot;:10&#125;</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">curl -H <span class="string">&#x27;user_role:owner&#x27;</span> http://localhost:3333/admin</span></span><br><span class="line">Forbidden</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">admin ロール</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">curl -H <span class="string">&#x27;user_role:admin&#x27;</span> http://localhost:3333/admin</span></span><br><span class="line">admin: index</span><br></pre></td></tr></table></figure></div>

<p>ロールに権限があれば、操作が成功していることが分かります。</p>
<h2 id="もっと細かい制御をするためには">もっと細かい制御をするためには</h2><p>上記の実装例だと、例えばある記事の削除は、<code>作成したユーザー自身</code> も削除できるようにしたい、といった要望には対応できません（ユーザー単位でロールを作ればもちろん可能ですが、もはやそれはロールの意味が無いですよね）。</p>
<p>Casbinでどう実現するかですが、今のURLの構造でがんばるのであれば、articlesが追加されるごとにユーザーIDとマッピングさせた policy.csv のレコードに相当するデータを釣っていくことです。Casbinの機能であれば、Priority Modelで多段の権限を判定できるので、参考になるかもしれません。</p>
<p>また、この実装例のURL階層だと、ワイルドカードが使いにくいので、 <code>/users/&lt;user_id&gt;/articles/&lt;article id&gt;</code> といった階層を工夫すると <code>policy</code> のメンテナンスをシンプルに抑えることもできるかと思います。</p>
<h2 id="まとめ">まとめ</h2><p>気になっていたCasbinの触りについてまとめました。ドキュメントサイトも新しくなっていますし、採用事例もちょくちょく聞きますし、プロダクション運用にも耐えうる品質だと思っています。</p>
<p>すこし気になっているのは、2022.10.3時点だと <code>v3.0.0-beta.7</code> がタグ付けされています。もうすぐv3系がリリースされる予感があり、APIの互換性がすこし崩れるのかも？ と推測しています（とは言え、ドキュメントページも整理されていますし、すでにv1→v2で移行しているので、根本からそこまで変わらないのでは？ と思っていますが）。このあたりは新規に採用する時に留意したほうが良いと思います。</p>
<p>アクセス制限は自前で作り込むと面倒な割に、ユーザーからは当たり前品質の扱いをされがちだと思うので、こういった既存プロダクトにうまくのっかれると良いと思います。</p>
<h2 id="参考">参考</h2><ul>
<li>https://qiita.com/unhurried/items/4688b4e94d96db2d1143</li>
<li>https://articles.wesionary.team/understanding-casbin-with-different-access-control-model-configurations-faebc60f6da5</li>
<li>https://zenn.dev/dove/articles/bc6933dbb39509</li>
</ul>
]]></content>
    <summary type="html">ACL、RBAC、ABACなどの様々なモデルでアクセス制御を行えるライブラリであるCasbinについて紹介します。</summary>
    <category term="認証認可" scheme="https://future-architect.github.io/categories/%E8%AA%8D%E8%A8%BC%E8%AA%8D%E5%8F%AF/"/>
    <category term="AuthZ" scheme="https://future-architect.github.io/tags/AuthZ/"/>
    <category term="Go" scheme="https://future-architect.github.io/tags/Go/"/>
    <category term="アクセス制御" scheme="https://future-architect.github.io/tags/%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E5%88%B6%E5%BE%A1/"/>
  </entry>
  <entry>
    <title>認証認可連載2022</title>
    <link href="https://future-architect.github.io/articles/20221003a/"/>
    <id>https://future-architect.github.io/articles/20221003a/</id>
    <published>2022-10-02T15:00:00.000Z</published>
    <updated>2022-10-02T15:00:00.000Z</updated>
    <author><name>admin</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2022/20221003a/padlock-g268bd2c48_640.jpg" alt="padlock-g268bd2c48_640.jpg" width="640" height="352">

<p>フューチャーで認証系のブログ連載と言えば、 Auth0 をテーマとした連載があります。今回は次のようにもう少しテーマを広くした連載を始めます。</p>
<ul>
<li>認証・認可の技術全般</li>
<li>WebAuthn、OpenID Connect 周り</li>
<li>KeyCloack、Auth0、AWS Cognitoなどなんでも</li>
</ul>
<p>2022年が初めてですので、「やってみた、触った見たレベルから」～「マニアックな挙動の紹介まで」何でもOKというテーマで募集したところ、5名が参加予定です。</p>
<div class="scroll"><table>
<thead>
<tr>
<th>Date</th>
<th>Title</th>
<th>Author</th>
</tr>
</thead>
<tbody><tr>
<td>9&#x2F;2</td>
<td>パスワードレスな認証を実現する認証ミドルウェアのhanko</td>
<td>澁川喜規</td>
</tr>
<tr>
<td>10&#x2F;4</td>
<td>Casbinで始めるアクセス制御</td>
<td>真野隼記</td>
</tr>
<tr>
<td>10&#x2F;6</td>
<td>Kong API Gatewayを使ってResource Serverを保護する</td>
<td>Lee</td>
</tr>
<tr>
<td>10&#x2F;7</td>
<td>Auth0のアクセストークン取得とITPへの対応</td>
<td>棚井龍之介</td>
</tr>
<tr>
<td>10&#x2F;12</td>
<td>OAuth の仕組みを理解しながらクライアントを実装してみる</td>
<td>吉岡朋哉</td>
</tr>
</tbody></table></div>
<p>フューチャーには「認証認可についての相談室」というGoogleチャットスペースがあり、そこで声掛けするとすぐに企画に賛同する人がいてくれて助かりました。</p>
<p>実はあまり活発ではないチャットルームなので、こうしたブログでの外部発表を通して交流を促進できると良いなとも感じています。</p>
<img src="/images/2022/20221003a/chat.png" alt="" width="385" height="75" loading="lazy">

<p>今回の連載で、少しでも皆様に良い情報を共有できればと思います！</p>
<hr>
<p>アイキャッチ画像は、Arek Socha from Pixabayを利用させていただきました。</p>
]]></content>
    <summary type="html">認証・認可の技術全般をテーマとして技術ブログ連載を始めます。2022年が初めての開催です。</summary>
    <category term="認証認可" scheme="https://future-architect.github.io/categories/%E8%AA%8D%E8%A8%BC%E8%AA%8D%E5%8F%AF/"/>
    <category term="AuthN" scheme="https://future-architect.github.io/tags/AuthN/"/>
    <category term="AuthZ" scheme="https://future-architect.github.io/tags/AuthZ/"/>
    <category term="インデックス" scheme="https://future-architect.github.io/tags/%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9/"/>
  </entry>
  <entry>
    <title>パスワードレスな認証を実現する認証ミドルウェアのhanko</title>
    <link href="https://future-architect.github.io/articles/20220902a/"/>
    <id>https://future-architect.github.io/articles/20220902a/</id>
    <published>2022-09-01T15:00:00.000Z</published>
    <updated>2022-09-01T15:00:00.000Z</updated>
    <author><name>澁川喜規</name></author>
    <content type="html"><![CDATA[<p>名前からすると日本の古き良き（悪名高い）デバイス認証方式のあれのように見えますが、パスワードレスな認証(passkey)を実現するOSSのプロダクトです。Go製でライセンスはAGPL3です。なかなか面白そうなので動かしてみました。</p>
<p>https://www.hanko.io/</p>
<p>このhankoのメンバーが運営しているpasskeys.ioというウェブサイトもあり、パスワードレスなログインを広めていこう！ という啓蒙サイトになっています。</p>
<p>https://www.passkeys.io/</p>
<p>この↑のサイトの存在を知らなかったのですが、@takuan_oshoさんからタレコミをいただきました。ありがとうございます。</p>
<h2 id="動かし方">動かし方</h2><p>READMEに書いてある通りにdocker composeで一通り必要なものを起動します。</p>
<div class="code-block"><figure class="highlight console"><input type="checkbox" id="code-wrap-nkytct-1" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-nkytct-1" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker compose -f deploy/docker-compose/quickstart.yaml</span></span><br><span class="line">   -p &quot;hanko-quickstart&quot; up --build</span><br></pre></td></tr></table></figure></div>

<p>デモサーバーが8888で起動するのでブラウザでアクセスしてみます。登録アプリでは最終的に秘密ページ（secured.html）を表示しているのですが、そこに至るまでのフローがいろいろ選べます。</p>
<p>ユーザー登録をする（パスキーの登録あり、なし）フローと、登録後にログイン（メールに送られてくる6桁コード or 指紋認証）でした。AndroidのPixel 4aとmacでは指紋認証でしたが、きっとWindowsだと顔認証とかも機種によって選べるのかもしれません。手元のZephyrus G14はカメラ無しなので試せませんでしたが。</p>
<img fetchpriority="high" src="/images/2022/20220902a/hanko.jpg" alt="hanko.jpg" width="941" height="1091">

<p>hanko自身はパスワード認証にも対応しているのですが、そのやり方はちょっとわからなかったです。まあhankoの目玉機能は一通り試せた感じです。YubiKeyを入れてみたけど登録手段としては表示されませんでした。READMEによるとまだYubiKey対応は開発中のようですね。</p>
<h2 id="docker-composeのシステム構成">docker composeのシステム構成</h2><p>サンプルプロジェクトのdocker-composeを見ると結構たくさんコンポーネントを使っています。こんなに全部必要なのか？ みたいに思ったので軽くみてみました。</p>
<ul>
<li>hanko-migrate</li>
<li>hanko</li>
<li>postgresd</li>
<li>hankojs</li>
<li>example</li>
<li>mailslurper</li>
</ul>
<p>このうち、hanko-migrateとhankoは同じイメージのオプション違いです。hankoサーバーがmigrateオプションを付けるとDBマイグレーションが起動するような仕組みになっているようです。DBマイグレーションで使っているライブラリは以下のやつでした。</p>
<ul>
<li>https://github.com/gobuffalo/fizz</li>
</ul>
<p>postgresdはお馴染みのPostgreSQLです。対応しているデータベースは以下の4つです。</p>
<ul>
<li>CockroachDB</li>
<li>MariaDB</li>
<li>MySQL</li>
<li>PostgreSQL</li>
</ul>
<p>hankojsは<code>&lt;hanko-auth/&gt;</code>というカスタムタグのJavaScriptライブラリをビルドして提供するためのサービスとなっています。アプリ開発する場合は<code>npm install @teamhanko/hanko-elements</code>すればいいので、必須コンポーネントというわけではなさそうです。カスタムタグなのでLitを使っているのかと思ってコードを見てみたらpreact-custom-elementを使っていました。preact製でちょっと残念。マイクロフロントエンド的にカスタムタグを使うのは面白いですね。</p>
<p>mailslurperは開発時に便利に使えるメールサーバー＆クライアントでした。今まではメール機能の開発は面倒なものだと思っていましたがこれは便利ですね。上記のフローで6桁のコードが送られてくる時に、localhost:8080にアクセスするとメール一覧のビューアーが表示されるのでここで6桁コードが取得できます。</p>
<p>https://www.mailslurper.com/</p>
<img src="/images/2022/20220902a/スクリーンショット_2022-08-15_17.43.41.png" alt="スクリーンショット_2022-08-15_17.43.41.png" width="1200" height="862" loading="lazy">

<p>いろいろコンポーネントがありましたが、JSはウェブフロントエンドにライブラリをバンドルしちゃえば不要ですし、DBとメールサーバーは本番環境ではSaaSサービスを使うでしょうし、マイグレーションは踏み台から起動すればいいだけなので、本番環境で起動すべきはhankoのバックエンドサーバーとアプリケーション本体だけでいけそうですね。</p>
<h2 id="サンプルサーバーの実装">サンプルサーバーの実装</h2><p>exampleはサンプルアプリケーションの本体です。ほとんど素のHTTPサーバーですが、ミドルウェアを1つ持っています。jwks.jsonをhanko本体のバックエンドサーバーから取得してきて認証の確認をしています。</p>
<div class="code-block"><figure class="highlight go"><input type="checkbox" id="code-wrap-nkytct-2" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-nkytct-2" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">SessionMiddleware</span><span class="params">()</span></span> echo.MiddlewareFunc &#123;</span><br><span class="line">	<span class="keyword">return</span> <span class="function"><span class="keyword">func</span><span class="params">(next echo.HandlerFunc)</span></span> echo.HandlerFunc &#123;</span><br><span class="line">		<span class="keyword">return</span> <span class="function"><span class="keyword">func</span><span class="params">(c echo.Context)</span></span> <span class="type">error</span> &#123;</span><br><span class="line">			cookie, err := c.Cookie(<span class="string">&quot;hanko&quot;</span>)</span><br><span class="line">			<span class="keyword">if</span> err == http.ErrNoCookie &#123;</span><br><span class="line">				<span class="keyword">return</span> c.Redirect(http.StatusTemporaryRedirect, <span class="string">&quot;/unauthorized&quot;</span>)</span><br><span class="line">			&#125;</span><br><span class="line">			<span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">				<span class="keyword">return</span> err</span><br><span class="line">			&#125;</span><br><span class="line">			set, err := jwk.Fetch(context.Background(), <span class="string">&quot;http://hanko:8000/.well-known/jwks.json&quot;</span>)</span><br><span class="line">			<span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">				<span class="keyword">return</span> err</span><br><span class="line">			&#125;</span><br><span class="line"></span><br><span class="line">			token, err := jwt.Parse([]<span class="type">byte</span>(cookie.Value), jwt.WithKeySet(set))</span><br><span class="line">			<span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">				<span class="keyword">return</span> c.Redirect(http.StatusTemporaryRedirect, <span class="string">&quot;/unauthorized&quot;</span>)</span><br><span class="line">			&#125;</span><br><span class="line"></span><br><span class="line">			log.Printf(<span class="string">&quot;session for user &#x27;%s&#x27; verified successfully&quot;</span>, token.Subject())</span><br><span class="line"></span><br><span class="line">			<span class="keyword">return</span> next(c)</span><br><span class="line">		&#125;</span><br><span class="line">	&#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div>

<h2 id="docker-compose-yamlのテクニック">docker-compose.yamlのテクニック</h2><p>今回サンプルを見てみて面白かったのはdocker-composeの書き方ですね。DBの待ち合わせとかどうしようか、リトライしておけばOK?みたいに今までやっていたのですが、このサンプルの書き方は良いですね。依存の順番に並べ替えて該当箇所だけ抜き出したのが以下のサンプルになります。</p>
<figure class="highlight yaml"><figcaption><span>/deploy/docker-compose/quickstart.yaml</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="attr">postgresd:</span></span><br><span class="line">  <span class="attr">healthcheck:</span></span><br><span class="line">    <span class="attr">test:</span> <span class="string">pg_isready</span> <span class="string">-U</span> <span class="string">hanko</span> <span class="string">-d</span> <span class="string">hanko</span></span><br><span class="line">    <span class="attr">interval:</span> <span class="string">10s</span></span><br><span class="line">    <span class="attr">timeout:</span> <span class="string">10s</span></span><br><span class="line">    <span class="attr">retries:</span> <span class="number">3</span></span><br><span class="line">    <span class="attr">start_period:</span> <span class="string">30s</span></span><br><span class="line"><span class="attr">hanko-migrate:</span></span><br><span class="line">  <span class="attr">restart:</span> <span class="string">on-failure</span></span><br><span class="line">  <span class="attr">depends_on:</span></span><br><span class="line">    <span class="attr">postgresd:</span></span><br><span class="line">      <span class="attr">condition:</span> <span class="string">service_healthy</span></span><br><span class="line"><span class="attr">hanko:</span></span><br><span class="line">  <span class="attr">restart:</span> <span class="string">unless-stopped</span></span><br><span class="line">  <span class="attr">depends_on:</span></span><br><span class="line">    <span class="attr">hanko-migrate:</span></span><br><span class="line">      <span class="attr">condition:</span> <span class="string">service_completed_successfully</span></span><br></pre></td></tr></table></figure>

<p>まずpostgresdではヘルスチェックを設定しています。DBマイグレーションはservice_healthyのpostgresdに依存、という書き方になっています。こういう書き方が可能なんですね。バックエンド本体はマイグレーションがservice_completed_successfullyの場合に起動となっています。マイグレーションと本体でそれぞれ一度起動なのかバッチなのかという違いがあるのでrestartの書き方が変えてあります。</p>
<h2 id="まとめ">まとめ</h2><p>まだベータということですが、hankoを動かして、ちょっとコードを読んでみました。</p>
<p>パスワードレスなログインは今後はエンタープライズなシステムでも主流になっていくと思いますが、それをコンパクトに実装したhankoは良い学習素材になってくれそうです。</p>
<p>本番運用で使うには、ユーザー登録の管理画面とかいろいろ周りに用意してあげないといけない気もします。また、本番環境ではAuth0などのSaaSを使ってローカル開発環境ではこちら、みたいな使い分けとかもありかもしれませんが、そのような組み合わせの実現にもいろいろノウハウは必要そうですので、サンプルの起動は簡単でも導入にはちょっと工夫が必要な気がします。</p>
<p>また、新規に新しく作られているプロダクトはいろいろモダンなテクニックを知るのにも良いですね。特にdocker-compose.yaml周りの知識のアップデートになりました。</p>
]]></content>
    <summary type="html">名前からすると日本の古き良き（悪名高い）デバイス認証方式のあれのように見えますが、パスワードレスな認証を実現するOSSのプロダクトです。Go製でライセンスはAGPL3です。なかなか面白そうなので動かしてみました。このhankoのメンバーが運営しているpasskeys.ioというウェブサイトもあり、パスワードレスなログインを広めていこう！という啓蒙サイトになっています。</summary>
    <category term="認証認可" scheme="https://future-architect.github.io/categories/%E8%AA%8D%E8%A8%BC%E8%AA%8D%E5%8F%AF/"/>
    <category term="Go" scheme="https://future-architect.github.io/tags/Go/"/>
    <category term="IDaaS" scheme="https://future-architect.github.io/tags/IDaaS/"/>
    <category term="MailSlurper" scheme="https://future-architect.github.io/tags/MailSlurper/"/>
    <category term="WebAuthn" scheme="https://future-architect.github.io/tags/WebAuthn/"/>
    <category term="パスキー" scheme="https://future-architect.github.io/tags/%E3%83%91%E3%82%B9%E3%82%AD%E3%83%BC/"/>
  </entry>
  <entry>
    <title>Auth0アカウントでShopifyにSSOする</title>
    <link href="https://future-architect.github.io/articles/20211110a/"/>
    <id>https://future-architect.github.io/articles/20211110a/</id>
    <published>2021-11-09T15:00:00.000Z</published>
    <updated>2021-11-09T15:00:00.000Z</updated>
    <author><name>武田拓己</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2021/20211110a/サムネイル.png" alt="サムネイル.png" width="462" height="288">

<h2 id="はじめに">はじめに</h2><p>はじめまして。2021年4月新卒入社、TIGの武田です。入社して早半年、最近開発の面白さに気付かされ、巷のエンジニアあるあるにも3割くらい共感できるようになりました。</p>
<p>私が参画した案件で、Auth0に登録されているエンドユーザ向けのアカウントを用いてShopifyにSSOする検証をしたので、今回はその方法をご紹介します。</p>
<h3 id="SSOとは？">SSOとは？</h3><p><strong>一度のユーザ認証</strong>をすると、以後そのユーザ認証に紐づけられているサービスを、追加の認証なしで利用できる機能です。<br>これにより、**ユーザはパスワードの記憶や管理の負担が減り、システム管理者はセキュリティ上の弱点を削減できます。</p>
<h3 id="Auth0とは？">Auth0とは？</h3><p>Auth0導入編をご参照ください。他にもAuth0関連の記事があります。</p>
<h3 id="Shopifyとは？">Shopifyとは？</h3><p>本格的なネットショップが開設できるECプラットフォームで、世界NO. 1のシェアを誇っています。詳しくは公式サイトをご覧ください。</p>
<h2 id="前提条件">前提条件</h2><p>実環境でSSO機能を利用するためには、ShopifyPlusのサブスクリプションが必要となります。また、Shopifyには無料の開発者向けの環境が用意されており、様々な機能をテストできます。今回は、開発者用ストアを使ってSSOを実装していきます。</p>
<p>Auth0のアカウントも必要になります。こちらも無料のものが提供されているので、今回はそちらを使います。</p>
<h2 id="サンプル実装">サンプル実装</h2><p>マルチパスを利用して、Auth0アカウントでShopifyにSSOできるよう実装していきます。</p>
<p><strong>目次</strong></p>
<p>1. Shopifyアカウントでマルチパスを有効にする</p>
<p>2. Auth0アプリケーションを作成し、URIを設定する</p>
<p>3. Auth0ルールを追加して、マルチパストークンを作成する</p>
<p>4. ShopifyテーマにAuth0リンクを設定する</p>
<h3 id="Shopifyアカウントでマルチパスを有効にする">Shopifyアカウントでマルチパスを有効にする</h3><p>Shopifyストアにログインし、 <code>設定</code>に移動して <code>チェックアウト</code>ウィンドウをクリックします。顧客アカウントを、任意または必須に設定することで、ストアでマルチパスを有効にできます。</p>
<img src="/images/2021/20211110a/技術ブログ①.png" alt="技術ブログ①.png" width="908" height="512" loading="lazy">

<p>このシークレットキーはマルチパスリクエストが正当であることを確認するための暗号を作成するために使用されます。シークレットキーを再発行したい場合、マルチパスを無効にしてから再度有効にすることで、新たなシークレットキーが生成され、以前のものは無効化されます（上記画像のシークレットキーは既に無効化済みです）。</p>
<h3 id="Auth0アプリケーションを作成し、URIを設定する">Auth0アプリケーションを作成し、URIを設定する</h3><p>Auth0ダッシュボード内で<code>Applications</code>に移動し、<code>Create Application</code>をクリックして適当な名前を付け（「Shopify Store」など）、<code>Regular Web Applications</code>を選択し、<code>CREATE</code>します。<br><img src="/images/2021/20211110a/技術ブログ②.png" alt="技術ブログ②.png" width="782" height="689" loading="lazy"></p>
<p><code>Settings</code>に移動します。</p>
<p>Application URIsを以下のように設定します。<br>{shopify-domain}は自身のストアのドメインに置き換える必要があります（例：sample-store.myshopify.com）</p>
<ul>
<li><strong>Application Login URI</strong>：https:&#x2F;&#x2F;{shopify-domain}&#x2F;account&#x2F;login</li>
<li><strong>Allowed Callback URLs</strong>：https:&#x2F;&#x2F;{shopify-domain}&#x2F;account</li>
<li><strong>Allowed Logout URLs</strong>：https:&#x2F;&#x2F;{shopify-domain}&#x2F;account&#x2F;logout<img src="/images/2021/20211110a/技術ブログ④.png" alt="技術ブログ④.png" width="976" height="755" loading="lazy"></li>
</ul>
<p><code>Advanced Settings</code>セクションを展開し、Application Metadataに次の2つのKeyとValueのペアを追加します。</p>
<ul>
<li><strong>Key</strong>：shopify_domain ; <strong>Value</strong>：{shopify-domain}</li>
<li><strong>Key</strong>：shopify_multipass_secret ; <strong>Value</strong>：{multipass-secret}<img src="/images/2021/20211110a/技術ブログ③.png" alt="技術ブログ③.png" width="969" height="648" loading="lazy"></li>
</ul>
<h3 id="Auth0ルールを追加して、マルチパストークンを作成する">Auth0ルールを追加して、マルチパストークンを作成する</h3><p>Auth0ダッシュボードの<code>Auth Pipeline</code>の<code>Rules</code>に移動して、<code>Create</code>を選択、templateは<code>Empty rule</code>を選択します。<br>わかりやすい名前（「ShopifyMultipass」など）を付け、次のコードを貼り付けます。</p>
<div class="code-block"><figure class="highlight javascript"><input type="checkbox" id="code-wrap-14fpqgl-1" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-14fpqgl-1" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="keyword">function</span> (<span class="params">user, context, callback</span>) &#123;</span><br><span class="line">  <span class="keyword">if</span> (context.<span class="property">clientMetadata</span> &amp;&amp; context.<span class="property">clientMetadata</span>.<span class="property">shopify_domain</span> &amp;&amp; context.<span class="property">clientMetadata</span>.<span class="property">shopify_multipass_secret</span>)</span><br><span class="line">  &#123;</span><br><span class="line">    <span class="keyword">const</span> <span class="variable constant_">RULE_NAME</span> = <span class="string">&#x27;shopify-multipasstoken&#x27;</span>;</span><br><span class="line">    <span class="keyword">const</span> <span class="variable constant_">CLIENTNAME</span> = context.<span class="property">clientName</span>;</span><br><span class="line">    <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class="string">`<span class="subst">$&#123;RULE_NAME&#125;</span> started by <span class="subst">$&#123;CLIENTNAME&#125;</span>`</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> now = (<span class="keyword">new</span> <span class="title class_">Date</span>()).<span class="title function_">toISOString</span>();</span><br><span class="line">    <span class="keyword">let</span> shopifyToken = &#123;</span><br><span class="line">      <span class="attr">email</span>: user.<span class="property">email</span>,</span><br><span class="line">      <span class="attr">created_at</span>: now,</span><br><span class="line">      <span class="attr">identifier</span>: user.<span class="property">user_id</span>,</span><br><span class="line">      <span class="attr">remote_ip</span>: context.<span class="property">request</span>.<span class="property">ip</span></span><br><span class="line">    &#125;;</span><br><span class="line">    <span class="keyword">if</span> (context.<span class="property">request</span> &amp;&amp; context.<span class="property">request</span>.<span class="property">query</span> &amp;&amp; context.<span class="property">request</span>.<span class="property">query</span>.<span class="property">return_to</span>)&#123;</span><br><span class="line">      shopifyToken.<span class="property">return_to</span> = context.<span class="property">request</span>.<span class="property">query</span>.<span class="property">return_to</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> hash = crypto.<span class="title function_">createHash</span>(<span class="string">&quot;sha256&quot;</span>).<span class="title function_">update</span>(context.<span class="property">clientMetadata</span>.<span class="property">shopify_multipass_secret</span>).<span class="title function_">digest</span>();</span><br><span class="line">    <span class="keyword">const</span> encryptionKey = hash.<span class="title function_">slice</span>(<span class="number">0</span>, <span class="number">16</span>);</span><br><span class="line">    <span class="keyword">const</span> signingKey = hash.<span class="title function_">slice</span>(<span class="number">16</span>, <span class="number">32</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> iv = crypto.<span class="title function_">randomBytes</span>(<span class="number">16</span>);</span><br><span class="line">    <span class="keyword">const</span> cipher = crypto.<span class="title function_">createCipheriv</span>(<span class="string">&#x27;aes-128-cbc&#x27;</span>, encryptionKey, iv);</span><br><span class="line">    <span class="keyword">const</span> cipherText = <span class="title class_">Buffer</span>.<span class="title function_">concat</span>([iv, cipher.<span class="title function_">update</span>(<span class="title class_">JSON</span>.<span class="title function_">stringify</span>(shopifyToken), <span class="string">&#x27;utf8&#x27;</span>), cipher.<span class="title function_">final</span>()]);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> signed = crypto.<span class="title function_">createHmac</span>(<span class="string">&quot;SHA256&quot;</span>, signingKey).<span class="title function_">update</span>(cipherText).<span class="title function_">digest</span>();</span><br><span class="line"></span><br><span class="line">    <span class="keyword">const</span> token = <span class="title class_">Buffer</span>.<span class="title function_">concat</span>([cipherText, signed]).<span class="title function_">toString</span>(<span class="string">&#x27;base64&#x27;</span>);</span><br><span class="line">    <span class="keyword">const</span> urlToken = token.<span class="title function_">replace</span>(<span class="regexp">/\+/g</span>, <span class="string">&#x27;-&#x27;</span>).<span class="title function_">replace</span>(<span class="regexp">/\//g</span>, <span class="string">&#x27;_&#x27;</span>);</span><br><span class="line"></span><br><span class="line">   context.<span class="property">redirect</span> = &#123;</span><br><span class="line">     <span class="attr">url</span>: <span class="string">`https://<span class="subst">$&#123;context.clientMetadata.shopify_domain&#125;</span>/account/login/multipass/<span class="subst">$&#123;urlToken&#125;</span>`</span></span><br><span class="line">   &#125;;</span><br><span class="line">  &#125;</span><br><span class="line">  <span class="keyword">return</span> <span class="title function_">callback</span>(<span class="literal">null</span>, user, context);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div>

<ul>
<li><strong>2行目</strong>：Auth0アプリケーションがshopify_domainとshopify_multipass_seceretのメタデータを保持しているときのみこのルールが実行されるようにします。</li>
<li><strong>4〜6行目</strong>：ルールが実行されていることを確認するためのロギングです。</li>
<li><strong>8-14行目</strong>：Shopifyには最低でもemailとcreated_atのデータが必要です。追加情報として、identifier（複数のAuth0アカウントが同じemailアドレスを持っている場合）、remote_ip（最初にログインリクエストを送信したコンピューターでのみこのマルチパスリクエストを使用できるようにする場合）を入れることができます。</li>
<li><strong>15〜17行目</strong>：return_toクエリ文字列に値がある場合は、これをShopifyトークンに追加します。</li>
<li><strong>19〜30行目</strong>：ここで実際に暗号化を行っています。GitHubのリポジトリを参照。</li>
<li><strong>32〜34行目</strong>：これにより、認証されたユーザの宛先が設定されます。<br>このルールが実行されると、ユーザはShopifyストアにリダイレクトされます。このルールの後にAuth0ルールがある場合、それらは完全にスキップされてしまうため、お気をつけください。</li>
</ul>
<img src="/images/2021/20211110a/技術ブログ⑤.png" alt="技術ブログ⑤.png" width="1059" height="856" loading="lazy">

<h3 id="ShopifyテーマにAuth0リンクを設定する">ShopifyテーマにAuth0リンクを設定する</h3><p>Shopifyテーマを編集してログイン&#x2F;ログアウトするためのリンクを追加していきます。<br>Shopifyストアの現在のテーマの<code>コードを編集</code>をクリックします。<br><img src="/images/2021/20211110a/技術ブログ⑥.png" alt="技術ブログ⑥.png" width="975" height="361" loading="lazy"></p>
<p>まずは、ログインページを編集してログインリンクを追加します。<code>Templates</code>フォルダ内の<code>customers/login.liquid</code>ファイルを開き、リンクを追加するのに適した場所を見つけます。今回は、<code>アカウント作成</code>リンクの下に以下のリンクを配置します。</p>
<div class="code-block"><figure class="highlight html"><input type="checkbox" id="code-wrap-14fpqgl-2" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-14fpqgl-2" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">a</span> <span class="attr">href</span>=<span class="string">&quot;&#123;&#123; settings.auth0_login_url &#125;&#125;&quot;</span>&gt;</span>Log in with Auth0<span class="tag">&lt;/<span class="name">a</span>&gt;</span></span><br></pre></td></tr></table></figure></div>

<img src="/images/2021/20211110a/技術ブログ⑦.png" alt="技術ブログ⑦.png" width="878" height="753" loading="lazy">

<p>次に、アカウントページを編集してログアウトリンクをAuth0のログアウトリンクに置き換えます。<code>Templates</code>フォルダ内の<code>customers/account.liquid</code>ファイルを開き、ログアウトリンクを以下のリンクに置き換えます。テーマ内の他の場所にもログアウトリンクがある場合は、それも同様に置き換える必要があります。</p>
<div class="code-block"><figure class="highlight html"><input type="checkbox" id="code-wrap-14fpqgl-3" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-14fpqgl-3" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">a</span> <span class="attr">href</span>=<span class="string">&quot;&#123;&#123; settings.auth0_logout_url &#125;&#125;&quot;</span>&gt;</span>log_out<span class="tag">&lt;/<span class="name">a</span>&gt;</span></span><br></pre></td></tr></table></figure></div>

<img src="/images/2021/20211110a/技術ブログ⑧.png" alt="技術ブログ⑧.png" width="943" height="328" loading="lazy">

<p>続いて、ユーザがログインURLとログアウトURLを貼り付けることができるようにテーマ設定を追加します。<code>Config</code>フォルダ内の<code>settings_schema.json</code>ファイルを開き、以下のスニペットを配列の最後に貼り付けます。</p>
<p>ここでは、「Auth0 Config」という新しい設定セクションを作成し、ログインURLとログアウトURLを入力できるようにしています。idプロパティは、上記のリンクで使用したプロパティの名前と一致させる必要があります。</p>
<div class="code-block"><figure class="highlight json"><input type="checkbox" id="code-wrap-14fpqgl-4" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-14fpqgl-4" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Auth0 Config&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;settings&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;text&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;id&quot;</span><span class="punctuation">:</span> <span class="string">&quot;auth0_login_url&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;label&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Auth0 Login Url&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;info&quot;</span><span class="punctuation">:</span> <span class="string">&quot;The full Auth0 URL to redirect the customer to for login.&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#123;</span></span><br><span class="line">      <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;text&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;id&quot;</span><span class="punctuation">:</span> <span class="string">&quot;auth0_logout_url&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;label&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Auth0 Logout Url&quot;</span><span class="punctuation">,</span></span><br><span class="line">      <span class="attr">&quot;info&quot;</span><span class="punctuation">:</span> <span class="string">&quot;The full Auth0 URL to redirect the customer to for logout.&quot;</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">  <span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure></div>

<img src="/images/2021/20211110a/技術ブログ⑨.png" alt="技術ブログ⑨.png" width="937" height="331" loading="lazy">

<p>続いて、URLを作成していきます。<br>まずは、以下のようにログインURLを作成します。</p>
<p>上記で作成したAuth0アプリケーションのClient IDを取得します。</p>
<img src="/images/2021/20211110a/技術ブログ⑩.png" alt="技術ブログ⑩.png" width="764" height="95" loading="lazy">

<ul>
<li><code>auth0-instance</code>：Auth0ドメイン（例：sample.jp.auth0.com）</li>
<li><code>clientid</code>：Auth0アプリケーションからの値。</li>
<li><code>shopify-domain</code>：自身のストアのドメイン。</li>
<li><code>return-to-path</code>：任意で返したいパスを設定可能（例：ログイン後にアカウントページに遷移させたい場合は、<code>account</code>と設定）。</li>
</ul>
<p><code>https://&#123;auth0-instance&#125;/authorize?response_type=code&amp;client_id=&#123;clientid&#125;&amp;return_to=https://&#123;shopify-domain&#125;/&#123;return-to-path&#125;&amp;scope=SCOPE&amp;state=STATE</code></p>
<p>同様にログアウトURLも作成します。</p>
<ul>
<li><code>auth0-instance</code>：Auth0ドメイン（例：sample.jp.auth0.com）</li>
<li><code>clientid</code>：Auth0アプリケーションからの値。</li>
<li><code>shopify-domain</code>：自身のストアのドメイン。</li>
</ul>
<p><code>https://&#123;auth0-instance&#125;.auth0.com/v2/logout?response_type=code&amp;client_id=&#123;clientid&#125;&amp;returnTo=https://&#123;shopify-domain&#125;/account/logout</code></p>
<p>テーマページに戻り、<code>カスタマイズ</code>をクリックして、画面左下に出てくる<code>テーマ設定</code>をクリック、<code>Auth0 Config</code>セクションを展開して、作成したURLを貼り付けます。</p>
<img src="/images/2021/20211110a/技術ブログ⑪.png" alt="技術ブログ⑪.png" width="1166" height="763" loading="lazy">

<p>以上で実装完了です！</p>
<h2 id="実際の画面遷移">実際の画面遷移</h2><p>ログインページにて、<code>Log in with Auth0</code>をクリックする。<br><img src="/images/2021/20211110a/技術ブログ⑫.png" alt="技術ブログ⑫.png" width="1200" height="707" loading="lazy"></p>
<p>上記で作ったShopify StoreというAuth0アプリケーションの認証画面が出てくるので、認証情報を入力してログインする。<br><img src="/images/2021/20211110a/技術ブログ⑬.png" alt="技術ブログ⑬.png" width="842" height="479" loading="lazy"></p>
<p>ログインに成功！<br><img src="/images/2021/20211110a/技術ブログ⑭.png" alt="技術ブログ⑭.png" width="1200" height="643" loading="lazy"></p>
<h2 id="さいごに">さいごに</h2><p>最近ではSSOを利用できるサービスがかなり増えてきたなという印象ですが、実際使ってみると本当に便利ですよね。他のアプリケーションでもこのような方法でSSOを導入できると思いますので、導入を検討する際にはこちらの記事を参考にしていただけますと幸いです。</p>
<p>最後まで読んでいただきありがとうございました！</p>
<h2 id="参考">参考</h2><ul>
<li>Authenticate Shopify Customers with Auth0 – Rovani in C#</li>
<li>Authenticate Shopify Customers with Auth0 - Shopify - Pavilion</li>
<li>Multipass | shopify.dev</li>
<li>multipassify&#x2F;multipassify.js at master · beaucoo&#x2F;multipassify</li>
<li>Auth0 Rule to Generate a Multipass token and redirect the user back to the Shopify store</li>
<li>Shopify PlusでSSO（シングルサインオン） - Qiita</li>
<li>Single Sign-On (SSO) For Shopify Using Auth0 as Identity Provider</li>
</ul>
]]></content>
    <summary type="html">私が参画した案件で、Auth0に登録されているエンドユーザ向けのアカウントを用いてShopifyにSSOする検証をしたので、今回はその方法をご紹介します。</summary>
    <category term="認証認可" scheme="https://future-architect.github.io/categories/%E8%AA%8D%E8%A8%BC%E8%AA%8D%E5%8F%AF/"/>
    <category term="Auth0" scheme="https://future-architect.github.io/tags/Auth0/"/>
    <category term="Auth0Rules" scheme="https://future-architect.github.io/tags/Auth0Rules/"/>
    <category term="SSO" scheme="https://future-architect.github.io/tags/SSO/"/>
  </entry>
  <entry>
    <title>Future Tech Night #14〜IDaaS/OSS/Managed比較〜</title>
    <link href="https://future-architect.github.io/articles/20210812b/"/>
    <id>https://future-architect.github.io/articles/20210812b/</id>
    <published>2021-08-11T15:00:01.000Z</published>
    <updated>2021-08-11T15:00:01.000Z</updated>
    <author><name>山田勇一</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2021/20210812b/key-2114046_1280.jpg" alt="" title="Arek SochaによるPixabayからの画像" width="800" height="450">

<h2 id="はじめに">はじめに</h2><p>Technology Innovation Group所属の山田です。2021年7月21日に Future Tech Night #14～認証認可（IDaaS）勉強会～で発表させてもらいました。</p>
<p>元々は、Rails Devise+cancancan、Cognito User Pools（5年前）、Auth0の開発経験があり、改めてOSSも加えて学んでみたかったのが、テーマを決めた背景になります。</p>
<p>なお、一緒に発表をした市川さんが、Auth0でWebAuthnを試されており、認証において非常に重要な機能になりますので、合わせてご覧ください。私はとても勉強になりました。</p>
<ul>
<li>Future Tech Night #14「生体認証・デバイス認証を活用するパスワードレスな認証規格「WebAuthn」を体験！」</li>
</ul>
<h2 id="資料">資料</h2><p>発表資料はこちらです。</p>


<h2 id="概要">概要</h2><h3 id="ハンズオン">ハンズオン</h3><p>全てのプロダクトをまっさらな状態からハンズオンし、要した時間と、利用できるまでの工程をまとめてみました。<br>アプリケーションはVueで統一しています。</p>
<p>ソースコードはコピー&amp;ペーストで動くを事を目指し、参考URLも掲載しています。</p>
<ul>
<li>Auth0<br>Auth0の初期設定、vueを利用したハンズオン</li>
<li>keycloak<br>keycloakの初期設定、vueを利用したハンズオン</li>
<li>Cognito<br>Cognitoの初期設定、Amplify＋Vueを利用したハンズオン、hosted UI＋Vueを利用したハンズオン</li>
</ul>
<h3 id="比較">比較</h3><ul>
<li>プラン<br>HPに掲載されている内容で、プランと価格を比較</li>
<li>機能<br>各プロダクトのダッシュボード画面、トップレベルメニューまでの機能比較</li>
</ul>
<h2 id="当日頂いたQA">当日頂いたQA</h2><p>時間の関係で頂いたQAに返答できなかったため、改めてこの場で返答させて頂きます。</p>
<p><strong>Q.</strong> Firebase Auth はフューチャーさんの方で事例や検証などされたりしていますでしょうか？（Auth0 が最も事例がある感じでしょうか）もし Firebase Authの事例などがあれば、どのような基準で選んでいるのか回答頂けると助かります。<br><strong>A.</strong> 私の周囲では、Keycloak、Auth0の採用が多いです。<br>理由の1つとして、SSOの実現が必須になるケースが多く、central authentication serviceの仕組みが欲しくなってしまう為です。Firebase Auth（は知識が不足しており、定かではありませんが）やAmplify(+cognito)は単一アプリで利用するには良い印象ですが、IDPとして使う為には、追加の実装が必要になるため、採用するケースが少ないように思います。</p>
<hr>
<p><strong>Q.</strong> Auth0を導入される際に比較されたIDaaS, 比較ポイントがもしあれば教えていただけないでしょうか。例えばOktaなどは比較されましたでしょうか？<br><strong>A.</strong> 残念ながら、Oktaとの比較結果は持ち合わせておらず、申し訳ありません。<br>比較ポイントとして特殊なものは無く、機能、非機能、価格、開発の自由度で純粋に比較しています。機能であれば、SSOやAD&#x2F;GSuiteなどとの統合、移行性、GDPRへの対応…etc<br>非機能であれば、認証スループット、可用性、データの所在…etc 等かと思います。</p>
<hr>
<p><strong>Q.</strong> IDaaSの選択肢として、Azure AD B2Cがどうか、私見で良いので聞きたいです。<br><strong>A.</strong> 勉強不足で申し訳ありません。Azure AD B2Cは初見でしたので機能を見てみました。<br>Customize性（Rules&#x2F;Hooks）、SDKの充実度などはAuth0が有利に見えますが、基本的な機能は揃っており、価格メリットがあれば十分選択肢になりうると思えました。</p>
<hr>
<p><strong>Q.</strong> Futureでの各サービスやOSSの採用事例とその際の選定基準などあればお聞きしたいです<br><strong>A.</strong> プロジェクトによって、優先すべき内容が異なるため、決まった選定基準はありません。<br>基本的にはプロジェクト単位に定められた機能、非機能の要件で選定軸を作り、第三者レビューも通した上で採用プロダクトを決めています。</p>
<h2 id="さいごに">さいごに</h2><p>次の機会があれば、追加で他のプロダクトも比較してみたいです。</p>
<p>ありがとうございました。</p>
]]></content>
    <summary type="html">2021年7月21日にFuture Tech Night #14～認証認可（IDaaS）勉強会～で発表させてもらいました。元々は、Rails Devise+cancancan、Cognito User Pools（5年前）、Auth0の開発経験があり、改めてOSSも加えて学んでみたかったのが、テーマを決めた背景になります。</summary>
    <category term="認証認可" scheme="https://future-architect.github.io/categories/%E8%AA%8D%E8%A8%BC%E8%AA%8D%E5%8F%AF/"/>
    <category term="Auth0" scheme="https://future-architect.github.io/tags/Auth0/"/>
    <category term="IDaaS" scheme="https://future-architect.github.io/tags/IDaaS/"/>
    <category term="Keycloak" scheme="https://future-architect.github.io/tags/Keycloak/"/>
    <category term="TechNight" scheme="https://future-architect.github.io/tags/TechNight/"/>
    <category term="登壇レポート" scheme="https://future-architect.github.io/tags/%E7%99%BB%E5%A3%87%E3%83%AC%E3%83%9D%E3%83%BC%E3%83%88/"/>
  </entry>
  <entry>
    <title>Future Tech Night #14「生体認証・デバイス認証を活用するパスワードレスな認証規格「WebAuthn」を体験！」</title>
    <link href="https://future-architect.github.io/articles/20210811b/"/>
    <id>https://future-architect.github.io/articles/20210811b/</id>
    <published>2021-08-10T15:00:01.000Z</published>
    <updated>2021-08-10T15:00:01.000Z</updated>
    <author><name>市川浩暉</name></author>
    <content type="html"><![CDATA[<img fetchpriority="high" src="/images/2021/20210811b/key-3348307_640.jpg" alt="" title="MasterTuxによるPixabayからの画像" width="640" height="360" loading="">

<h2 id="はじめに">はじめに</h2><p>こんにちは、TIGの市川浩暉です。</p>
<p>2021年7月21日にFuture Tech Night #14～認証認可（IDaaS）勉強会～ を開催し、「生体認証・デバイス認証を活用するパスワードレスな認証規格「WebAuthn」を体験！」というテーマで登壇させていただきました。</p>
<p>なお、登壇者の資料は こちら に公開済みですので、興味があればご参照ください。</p>
<p>一緒にイベントに登壇した山田さんのレポートはも公開されています。</p>
<ul>
<li>IDaaS(Auth0) vs OSS（Keycloak）vs Managed(Amazon Cognito)で使い勝手を確認</li>
</ul>
<p>参加申し込み数はこれまでのFuture Tech Night史上最多となる190名の申し込みをいただき、大盛況での開催となりました。</p>
<h2 id="発表内容">発表内容</h2>

<p>当日の発表では、以下のアジェンダに沿って発表を実施しました。</p>
<ul>
<li>自己紹介</li>
<li>WebAuthnの概要説明<ul>
<li>前置き</li>
<li>これまでの認証方式</li>
<li>FIDO（Fast IDentity Online）</li>
<li>登録、認証フロー</li>
<li>WebAuthnとは</li>
<li>2つの認証方式</li>
<li>WebAuthn対応ブラウザ</li>
<li>WebAuthnを利用するメリット・デメリット</li>
</ul>
</li>
<li>Auth0を用いたWebAuthnの構築</li>
<li>まとめ</li>
</ul>
<h2 id="発表の概要">発表の概要</h2><p>まず、WebAuthnが生まれた背景を理解しやすいよう、認証方式の変遷を説明しました。</p>
<p>その中で、パスワード認証方式と2要素認証の課題を解決するために生まれたFIDOという考え方、そしてFIDOをWebでも使用できるようにしたFIDO2（WebAuthn, CTAP）が生まれ、WebAuthnの登録と認証のフローについて説明しました。</p>
<p>WebAuthnの概要を理解した後に、最近Auth0がリリースした機能を用いて実際に生体認証によるパスワードレス機能、そして実装してみた感想を発表しました。</p>
<h2 id="Q-A">Q&amp;A</h2><h3 id="Webサーバーに公開鍵はどのタイミングで登録されるのでしょうか？">Webサーバーに公開鍵はどのタイミングで登録されるのでしょうか？</h3><p>登録されるタイミングはWebサーバ側で送られてきたチャレンジキーの検証に成功したタイミングです。<br>スライドのP.21にあるとおり、（6）にて生成した公開鍵を（7）でWebサーバ側に送信し、（8）での検証成功後に公開鍵とユーザの紐付けを行って登録します。</p>
<h3 id="実際の業務でWebAuthenを用いたAuth0での認証を使用した事例はありますか？-またもし利用するとしたらどのような事例でしょうか？">実際の業務でWebAuthenを用いたAuth0での認証を使用した事例はありますか？　またもし利用するとしたらどのような事例でしょうか？</h3><p>フューチャーではAuth0をIDaaSとして採用し、実際に本番環境にて運用しているケースは多いのですが、今回ご紹介した機能はリリースされたばかりということもあり、実際のプロジェクトでの導入までは至っておりません。</p>
<p>IDaaSとしてAuth0を採用する場合、今回ご説明したWebAuthnを用いる機能は要件として含めることは可能と考えており、機会があれば前向きに考えていきたいと考えております。</p>
<h2 id="所感">所感</h2><p>初めての勉強会登壇でしたが、アンケートでの回答やTwitterでのリアルタイム反応を見るのは新鮮で、自分にとって学びの多い勉強会になりました。反省点としては、少し時間がオーバしてしまい質疑応答ができなかったので、次回以降のイベントでは改善していければと思います。</p>
<p>フューチャーではFuture Tech Nightの他にも様々なイベントを開催しており、引き続き、参加者の皆さんと交流できる場としてもイベントを盛り上げていければと考えています。今後も皆様のご参加をお待ちしております。次回のイベント情報はフューチャーのconnpassで確認できます。</p>
<p>最後に、発表をご視聴いただいた方、当記事を最後まで読んでいただいた方、ありがとうございました。</p>
]]></content>
    <summary type="html">2021年7月21日にFuture Tech Night #14～認証認可（IDaaS）勉強会～を開催し、「生体認証・デバイス認証を活用するパスワードレスな認証規格「WebAuthn」を体験！」というテーマで登壇させていただきました。</summary>
    <category term="認証認可" scheme="https://future-architect.github.io/categories/%E8%AA%8D%E8%A8%BC%E8%AA%8D%E5%8F%AF/"/>
    <category term="Auth0" scheme="https://future-architect.github.io/tags/Auth0/"/>
    <category term="TechNight" scheme="https://future-architect.github.io/tags/TechNight/"/>
    <category term="WebAuthn" scheme="https://future-architect.github.io/tags/WebAuthn/"/>
    <category term="登壇レポート" scheme="https://future-architect.github.io/tags/%E7%99%BB%E5%A3%87%E3%83%AC%E3%83%9D%E3%83%BC%E3%83%88/"/>
  </entry>
  <entry>
    <title>AWS APIGateway Custom Authorizer入門</title>
    <link href="https://future-architect.github.io/articles/20210610a/"/>
    <id>https://future-architect.github.io/articles/20210610a/</id>
    <published>2021-06-09T15:00:00.000Z</published>
    <updated>2021-06-09T15:00:00.000Z</updated>
    <author><name>李光焄</name></author>
    <content type="html"><![CDATA[<p>こんにちは。TIG&#x2F;DXユニットのLEEです。フューチャーではここ数年、主に認証認可関係の設計や開発などを担当しております。</p>
<p>今回は流行りの認証プロトコルであるOpenID ConnectとOAuth2.0におけるAuthorizerについて話そうと思います。</p>
<h2 id="Authorizerとは">Authorizerとは</h2><img fetchpriority="high" alt="カスタムオーソライザの動作フロー" src="/images/2021/20210610a/custom-auth-workflow.png" width="800" height="450">

<p>AuthorizerとはAWS APIGatewayにある機能の1つで、外からAPIサーバに送られてくるリクエストを検証することにより、アクセスを制御する機能です。OAuth2.0のプロトコルにおいては、AuthorizerはJWTなどTokenを検証することで、APIサーバ、つまり <code>ResourceServer</code> を保護する役割を持っています。</p>
<p>OSSのAPIGatewayであるKongを触ったことがある方ならば、JWT Pluginとほぼ同じ立ち位置のものと思って構いません。</p>
<h2 id="なぜ使うのか">なぜ使うのか</h2><p>SinglePageApplicationやモバイルアプリなど、ClientになるFront-endがサーバと分離されたシステム構成の場合、<code>Client (RelyingParty)</code> と <code>APIサーバ (ResourceServer)</code> を両方セキュアにする必要があります。</p>
<p><code>RelyingParty</code> の場合、KeycloakやAuth0など認証基盤が提供するライブラリや、OIDCに準拠したライブラリを使えば割と簡単にセキュアにできます。</p>
<p>一方、<code>ResourceServer</code> にはAuthorizerを実装する必要があります。Authorizerはサーバの内部のMiddleware層などにも実装できますが、複数のAPIサーバが存在してて、1つのAPIGatewayでEndpointを集中管理する場合にAuthorizerをLambdaとして一本実装することにより、開発やデプロイなどにメリットをもたらすことができます。</p>
<p>この度はそのAuthorizerを実装するにあたって、いくつか考慮すべきポイントについて触れて行こうと思います。</p>
<h2 id="Authorizer設定">Authorizer設定</h2><h3 id="タイプ">タイプ</h3><p>今回はCognitoではなく、KeycloakやAuth0など外部の認証基盤を想定しています。<br>AuthorizerをLambda関数で実装することにより、認証認可制御をもっと自由にカスタムできます。</p>
<h3 id="Lambdaイベントペイロード">Lambdaイベントペイロード</h3><p>Lambda関数の引数となるEventの入力値には2パターン存在します。</p>
<ul>
<li><strong>Tokenタイプ</strong>は簡単にTokenとmethodArnのみが取得可能で、Tokenを検証しその(JWTならば)PayloadとmethodArnのパスを対照するなどで認可を制御することが可能になります。</li>
<li><strong>Requestタイプ</strong>はAPIGatewayのプロキシ統合のリクエストと同じものを引数として受けられます。TokenとmethodArnはもちろん、他のHeaderやクエリ文字列、Bodyなどすべてのリクエストの中身が取得できるため、もう少し自由な認可要件が必要なときに使うこともできます。</li>
</ul>
<h3 id="トークンの検証">トークンの検証</h3><p>Tokenの中身を検証する前に正規表現により簡単にチェックできます。一般的にTokenとしてJWTを使う場合は<code>^Bearer [-_0-9a-zA-Z.]+$</code>のように設定します。<br>この正規表現にマッチしない場合、AuthorizerはLambdaまでリクエストを送らず401を返します。</p>
<h3 id="認可のキャッシュ">認可のキャッシュ</h3><p>AuthorizerはAPIリクエストが送られるとき毎回必ずTokenを検証するので、その負荷を減らすためにキャッシングも可能です。<br>しかし、この機能には大きな問題があり、キャシングの単位がTokenそのものではなく、Tokenのソースであるヘッダー名(<code>Authorization</code>など)になっています。あるユーザが一度認可したあとならば、他のユーザがそのキャッシュを使い回すことができてしまうため、基本無効にするしかないと思います。</p>
<h2 id="Lambdaの実装">Lambdaの実装</h2><p>Lambda実装の流れは大きく分けて</p>
<ol>
<li>まず、Tokenを検証し<strong>認証</strong>する</li>
<li>検証したTokenのPayloadとアクセスしようとするリソースの情報(methodArnなど)を対照し<strong>認可</strong>する</li>
</ol>
<p>の2つの段階になるかと思います。</p>
<h3 id="入出力">入出力</h3><p>Goで実装する場合メインハンドラー関数は以下のような形になります。</p>
<div class="code-block"><figure class="highlight go"><input type="checkbox" id="code-wrap-1km1lje-1" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1km1lje-1" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Handle</span><span class="params">(e events.APIGatewayCustomAuthorizerRequest)</span></span> (*events.APIGatewayCustomAuthorizerResponse, <span class="type">error</span>) &#123;</span><br><span class="line">    <span class="comment">// Tokenタイプイベントペイロード</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Handle</span><span class="params">(e events.APIGatewayCustomAuthorizerRequestTypeRequest)</span></span> (*events.APIGatewayCustomAuthorizerResponse, <span class="type">error</span>) &#123;</span><br><span class="line">    <span class="comment">// Requestタイプイベントペイロード</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></div>

<p>aws-lambda-goには、すでにCustomAuthorizerのための入出力構造体が用意されているため大変便利です。<br>出力の戻り値としてはAWS IAMのようなAWSPolicyDocumentを使い返します(詳細後述)。</p>
<h4 id="出力パターン">出力パターン</h4><p>Lambda関数の出力(戻り値)により、以下のようにAPIに送られてきたリクエストを制御できます。</p>
<div class="scroll"><table>
<thead>
<tr>
<th>出力パターン</th>
<th>動作</th>
<th>HTTP Status</th>
<th>Response Body</th>
</tr>
</thead>
<tbody><tr>
<td>Policy：Allow</td>
<td>アクセス許可</td>
<td>後続のAPIレスポンスによる</td>
<td>後続のAPIレスポンスによる</td>
</tr>
<tr>
<td>Policy：Deny</td>
<td>認可失敗</td>
<td>403 Forbidden</td>
<td><code>&#123;&quot;message&quot;: &quot;User is not authorized to access this resource with an explicit deny&quot;&#125;</code></td>
</tr>
<tr>
<td>Error：Unauthorized</td>
<td>認証失敗</td>
<td>401 Unauthorized</td>
<td><code>&#123;&quot;message&quot;: &quot;Unauthorized&quot;&#125;</code></td>
</tr>
<tr>
<td>その他のError</td>
<td>エラー</td>
<td>500 Internal Server Error</td>
<td><code>&#123;&quot;message&quot;: &quot;Internal Server Error&quot;&#125;</code></td>
</tr>
</tbody></table></div>
<p><em>特記事項として、エラーを返すにしてもエラーメッセージを<code>Unauthorized</code> (大文字<code>U</code>に注意)にすることにより401を返すことができます。</em></p>
<h3 id="Authentication">Authentication</h3><p>認可制御のために前提として、まずは認証が必要になります。一般的にはJWTを検証することになり、JWTのライブラリを使えば簡単ですが、検証のための<strong>公開鍵取得方法</strong>には2パターンがあるかと思われます。</p>
<h4 id="静的に公開鍵を保持する">静的に公開鍵を保持する</h4><p>公開鍵をLambdaの環境変数やDynamoDB、S3などを使い静的に保持する方法です。<br>実装は簡単で構造もシンプルですが、鍵のローテションをどうするか考える必要が将来的に出てきます。</p>
<h4 id="公開鍵を動的に取得する">公開鍵を動的に取得する</h4><p>認証基盤が公開している公開鍵エンドポイントから鍵を取得する方法です。<br>公開鍵エンドポイントは一般的に認証基盤側がJSON Web Key(JWK)により定義し、以下のような形で公開しています。</p>
<ul>
<li>Keycloak証明書エンドポイント</li>
<li>Auth0 JSON Web Key Sets</li>
</ul>
<p>この方法は鍵のローテションを気にせずに済みますが、APIリクエストのたびに認証基盤への外部リクエストが発生するので、遅延・負荷を軽減するための効率的なキャッシング戦略を立てる必要があります。</p>
<h3 id="Authorization">Authorization</h3><p>APIリクエストのToken検証が完了し認証ができたら、次はそのユーザーがリクエストしたエンドポイントにアクセス可能かをチェックする認可処理が必要になります。</p>
<p>認可、アクセスコントロールはJWTのClaimsの値に入っているユーザの属性やロールとAPIエンドポイントのパスなどを対照することにより制御できます。</p>
<p>ロジックについては認証基盤の設定やそのシステムの固有の考え方などによりRole-BasedAccessControl、Attribute-BasedAccessControlなど、様々なやり方があります。こういったロジックは自由度の高い領域なのでここでは参考程度にKeycloakやAuth0などで想定しているアクセスコントロールについてのリンクだけを貼っておきます。</p>
<ul>
<li>Keycloak Authorization Services Guide</li>
<li>Auth0 Authorization</li>
</ul>
<h4 id="認可の出力">認可の出力</h4><p>認可ロジックによりユーザのアクセス可否が決まったら、Authorizerは以下のようなJSONで認可処理が完了したことをAPIGatewayに返します。</p>
<div class="code-block"><figure class="highlight json"><input type="checkbox" id="code-wrap-1km1lje-2" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-1km1lje-2" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;principalId&quot;</span><span class="punctuation">:</span> <span class="string">&quot;yyyyyyyy&quot;</span><span class="punctuation">,</span> <span class="comment">// The principal user identification associated with the token sent by the client.</span></span><br><span class="line">  <span class="attr">&quot;policyDocument&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;Version&quot;</span><span class="punctuation">:</span> <span class="string">&quot;2012-10-17&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;Statement&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">      <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;Action&quot;</span><span class="punctuation">:</span> <span class="string">&quot;execute-api:Invoke&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;Effect&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Allow|Deny&quot;</span><span class="punctuation">,</span></span><br><span class="line">        <span class="attr">&quot;Resource&quot;</span><span class="punctuation">:</span> <span class="string">&quot;arn:aws:execute-api:&#123;regionId&#125;:&#123;accountId&#125;:&#123;apiId&#125;/&#123;stage&#125;/&#123;httpVerb&#125;/[&#123;resource&#125;/[&#123;child-resources&#125;]]&quot;</span></span><br><span class="line">      <span class="punctuation">&#125;</span></span><br><span class="line">    <span class="punctuation">]</span></span><br><span class="line">  <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;context&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;stringKey&quot;</span><span class="punctuation">:</span> <span class="string">&quot;value&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;numberKey&quot;</span><span class="punctuation">:</span> <span class="string">&quot;1&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;booleanKey&quot;</span><span class="punctuation">:</span> <span class="string">&quot;true&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;usageIdentifierKey&quot;</span><span class="punctuation">:</span> <span class="string">&quot;&#123;api-key&#125;&quot;</span> <span class="comment">// Optional</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure></div>

<h5 id="Policy-Document">Policy Document</h5><p>IAMのものと同じ形式で、アクセスを許可するか拒否するかを明示的に表現します。</p>
<p>AuthorizerはAPIGateway上で動くものなので<code>&quot;Action&quot;: &quot;execute-api:Invoke&quot;</code>は固定になります。<br><code>Resource</code>はLambda関数の引数で受けた<code>methodArn</code>をそのまま返すで問題ありません。</p>
<h5 id="Principal-ID">Principal ID</h5><p>APIリクエストしたユーザが誰なのかを表現します。リクエストしたユーザを一意に識別するための値であり、実際のAPIロジックを決める後続のLambda関数などに渡すことができ、ユーザによるレスポンスの出し分けなどを可能にします。</p>
<p>一般的にはJWTの<code>sub</code> (Subject) Claimをそのまま使うことになります。</p>
<h5 id="Authorizer-Context">Authorizer Context</h5><p>Principal IDと同じように後続のLambda関数などに渡すことができる任意の値です(Principal IDもContextの一部)。APIのレスポンスを出し分けするために必要な任意の情報をkey-value形式でセットできます。一見Mapオブジェクトにも見えますが、ValueとしてはNumber・String・BooleanのみでObjectやArrayなどの入れ子構造は使えません。</p>
<h2 id="さいごに">さいごに</h2><p>Authorizerの実装、最初はわからないことだらけで難しく感じるかもしれませんが、単機能の関数であるため、一度実装してしまったらテンプレートのように様々なAPIに使い回すことも可能かと思います。</p>
<p>以下は自分が実装の際に一番参考になったサンプルコードのリンクを置いて締めたいと思います。</p>
<ul>
<li>Amazon API Gateway - Custom Authorizer Blueprints for AWS Lambda</li>
<li>AWS Lambda for Go - Authorizer Sample Function</li>
<li>Auth0 Backend&#x2F;API Go: Authorization</li>
</ul>
]]></content>
    <summary type="html">今回は流行りの認証プロトコルであるOpenID ConnectとOAuth2.0におけるAuthorizerについて話そうと思います。AuthorizerとはAWS APIGatewayにある機能の一つで、外からAPIサーバに送られてくるリクエストを検証することにより、アクセスを制御する機能です。</summary>
    <category term="認証認可" scheme="https://future-architect.github.io/categories/%E8%AA%8D%E8%A8%BC%E8%AA%8D%E5%8F%AF/"/>
    <category term="APIGateway" scheme="https://future-architect.github.io/tags/APIGateway/"/>
    <category term="AWS" scheme="https://future-architect.github.io/tags/AWS/"/>
    <category term="JWT" scheme="https://future-architect.github.io/tags/JWT/"/>
    <category term="Lambda" scheme="https://future-architect.github.io/tags/Lambda/"/>
  </entry>
  <entry>
    <title>Auth0でADをユーザDBにし、SalesforceとのSSOを確認する</title>
    <link href="https://future-architect.github.io/articles/20210302/"/>
    <id>https://future-architect.github.io/articles/20210302/</id>
    <published>2021-03-01T15:00:00.000Z</published>
    <updated>2021-03-01T15:00:00.000Z</updated>
    <author><name>山田勇一</name></author>
    <content type="html"><![CDATA[<p>エンタープライズの領域ではAD認証が多く利用されており、また同時にCRMとしてSalesforceが導入されているケースが多くあります。<br>この場合、社内システムにおける「統合認証」の要件として、これらを繋げてログインする必要が出てきます。</p>
<p>これらの要求に対応するため、以下2点を確認し、Active Directory（以降AD）を中心とした統合認証を試してみます。</p>
<ol>
<li>Auth0のApplicationsでAD認証ができることを確認</li>
<li>SalesforceのSSO機能を利用し、Auth0経由でAD認証かつSSOができることを確認</li>
</ol>
<h2 id="Auth0とは？">Auth0とは？</h2><img fetchpriority="high" src="/images/2021/20210222/top.png" class="img-middle-size" width="534" height="192">

<p>Auth0導入編をぜひ参照ください。他にもAuth0関連の記事があります。</p>
<h2 id="Auth0に「Active-Directory-LDAP」Connectorを追加">Auth0に「Active Directory &#x2F; LDAP」Connectorを追加</h2><h3 id="設定追加">設定追加</h3><p><code>メニュー　-&gt; Connections -&gt; Enterprise -&gt; Active Directory / LDAP -&gt; CREATE CONNECTION</code><br>メニューからConnectorを追加し、今回は2つのオプションを有効にしています。</p>
<ul>
<li>Use Windows Integrated Auth (Kerberos)<br>Auth0はWindows統合認証（Kerberos認証）に対応しており、WindowsでAD認証でログインしており、かつ <code>IP Ranges</code> のIPでログインすると認証をスキップできます。</li>
<li>Sync user profile attributes at each login<br>こちらはシンプルに認証時に最新のプロファイルをADから取得できる設定となっています。</li>
</ul>
<img src="/images/2021/20210302/スクリーンショット_2021-02-24_10.03.37.png" class="bordered" width="1200" height="1254" loading="lazy">

<h2 id="ADサーバーの設定">ADサーバーの設定</h2><h3 id="Connector設定確認">Connector設定確認</h3><p>追加済みのConnectorより、「Setup」タブを確認し <code>Ticket Url</code> を控えておきます。<br><strong>この<code>Ticket Url</code>がADサーバーの設定に必要となります。</strong></p>
<img src="/images/2021/20210302/スクリーンショット_2021-02-24_10.06.35.png" class="bordered" width="1200" height="638" loading="lazy">

<h3 id="ADサーバーにAD-LDAP-Connectorをインストール">ADサーバーにAD LDAP Connectorをインストール</h3><p>インストール手順を参考に、ウィザードに従ってインストールしてください。<br>インストール時に前述の手順で控えた<code>Ticket Url</code>が必要になります。</p>
<h3 id="AD-LDAP-Connectorの設定を変更">AD LDAP Connectorの設定を変更</h3><p>Auht0らしく、AD LDAP Connectorの設定をスクリプトで変更できる部分があります。<br>ProfileMapper（ADのユーザプロファイルとAuth0のユーザプロファイルのマッピング）のタブが、スクリプトで記載できる設定になっており、今回は詰められる情報を最大まで詰めてみました。<br>ここで設定したプロファイルがログイン時にAuth0に送信される情報となります。</p>
<img src="/images/2021/20210302/スクリーンショット_2020-09-11_17.49.51.png" class="bordered" width="1200" height="919" loading="lazy">

<h3 id="ADとAuth0が接続できていることを確認">ADとAuth0が接続できていることを確認</h3><p>Auth0側の<code>Connections</code>の表示が、<code>Offline</code>から<code>Online</code>に変化します。</p>
<img src="/images/2021/20210302/スクリーンショット_2020-09-11_9.36.28.png" class="bordered" width="1200" height="175" loading="lazy">

<h2 id="Applicationsでログイン確認">Applicationsでログイン確認</h2><h3 id="Applicationsの設定変更">Applicationsの設定変更</h3><h3 id="ApplicationsでConnectionsを有効化">Applicationsで<code>Connections</code>を有効化</h3><p>Applicationsの設定で<code>Connections</code>タブを開き、設定済みのADを有効化します。</p>
<img src="/images/2021/20210302/スクリーンショット_2021-02-22_18.59.51.png" class="bordered" width="1200" height="1033" loading="lazy">

<h3 id="ログインを確認">ログインを確認</h3><p>サンプルアプリケーションを利用し、ログイン後のプロファイルを確認します。<br>ここで、ADで設定済みのプロファイルが見えれば連携成功です。</p>
<img src="/images/2021/20210302/スクリーンショット_2020-09-11_15.33.11.png" class="bordered" width="1200" height="497" loading="lazy">

<h3 id="プロファイルが取れるか確認">プロファイルが取れるか確認</h3><p>Auth0のRulesでプロファイルの取得を入れ込み、結果を見ます。</p>
<img src="/images/2021/20210302/スクリーンショット_2020-09-11_17.59.05.png" class="bordered" width="1200" height="612" loading="lazy">

<p>ADサーバーのAD LDAP Connectorで指定した情報が取れていることがわかります。<br>なお、ここまで確認できればAuth0上でユーザ情報を自由に扱えそうだと判断できます。<br>例えば、ログイン時にADからユーザ情報を透過的に移行するなどの対応も考えられます。</p>
<img src="/images/2021/20210302/スクリーンショット_2020-09-11_15.38.32.png" width="1200" height="858" loading="lazy">

<img src="/images/2021/20210302/スクリーンショット_2020-09-11_15.38.48.png" width="1200" height="575" loading="lazy">

<h2 id="Salesforceの外部認証にAuth0を設定">Salesforceの外部認証にAuth0を設定</h2><h3 id="Salesforceのアカウント準備">Salesforceのアカウント準備</h3><p>SSOの前提として、Auth0のドメインを設定する必要があります。</p>
<h3 id="Salesforce側にADとSSOさせたいユーザを作成">Salesforce側にADとSSOさせたいユーザを作成</h3><p><strong>SalesforceのSSOでは、Salesforce側に事前にSSOしたユーザの登録が必要です。</strong><br>また、SSOさせる場合にSalesforceのユーザとADのユーザで、SSOに利用する属性情報を一致させる必要があります。<br>とはいえ、Auth0のログイン画面を使う場合、ADとSalesforceで一致させる属性はEmailが最善です。<br>今回はこの青枠ユーザをSSOで利用します。</p>
<img src="/images/2021/20210302/スクリーンショット_2021-02-22_19.33.57.png" class="bordered" width="1200" height="275" loading="lazy">

<h3 id="Saleforceのドメイン設定">Saleforceのドメイン設定</h3><p>SSOにはドメイン設定が必要になるため、設定しておきます。<br>ここでAuth0に移ります。</p>
<h3 id="auth0にSalesforce用のSSO設定を追加">auth0にSalesforce用のSSO設定を追加</h3><p><code>SSO Integrations</code>から<code>CREATE SSO INTEGRATION</code>を選択し、SalesforceのSSO設定を追加します<br>Salesforce側のドメインが必要になるので、Auth0の設定ページを確認しつつSalesforceから情報を取得してください。</p>
<img src="/images/2021/20210302/スクリーンショット_2021-02-22_18.59.24.png" class="bordered" width="1200" height="265" loading="lazy">

<p>Salesforceのドメインに<code>https://</code>をつけたものが<code>Entity ID</code>になります。</p>
<img src="/images/2021/20210302/スクリーンショット_2021-02-22_18.59.45.png" class="bordered" width="1200" height="929" loading="lazy">

<p>追加設定として、認証先をADに変更します。</p>
<img src="/images/2021/20210302/スクリーンショット_2021-02-22_18.59.51_2.png" class="bordered" width="1200" height="1033" loading="lazy">

<p>ここで、Salesforceに移ります。</p>
<h3 id="SaleforceのSSO設定追加">SaleforceのSSO設定追加</h3><p>メニューの<code>ID-&gt;シングルサインオン設定</code>を選択し、<code>新規</code>から接続設定を作ります。</p>
<img src="/images/2021/20210302/スクリーンショット_2021-02-22_19.09.12.png" width="1200" height="996" loading="lazy">

<p>Auth0のSalesforce設定ページにチュートリアルページあるので、手順に従い必須項目を埋めます。<br><code>IDはattribute要素にあります</code>を選択し、<code>email</code>を入力することを忘れないでください。<br>設定した<code>email</code>が、ADとSalesforceでSSOさせるユーザの一致属性となります。</p>
<img src="/images/2021/20210302/スクリーンショット_2021-02-22_19.44.06.png" class="bordered" width="1200" height="614" loading="lazy">

<h3 id="SSOの確認">SSOの確認</h3><p>これでようやく設定完了です。<br>追加したSSOのログインボタンが現れますので、自ドメインの認証画面からSSOユーザでログインしてください。</p>
<img src="/images/2021/20210302/スクリーンショット_2020-09-14_12.52.42.png" width="1200" height="790" loading="lazy">

<p>ログインできれば成功です。<br>お疲れ様でした。</p>
<ul>
<li>Auth0で認証成功後に任意のWebページを表示させたい | フューチャー技術ブログ</li>
<li>Auth0 EmailまたはSMSを使ったパスワードレス認証を設定する | フューチャー技術ブログ</li>
</ul>
]]></content>
    <summary type="html">エンタープライズの領域ではAD認証が多く利用されており、また同時にCRMとしてSalesforceが導入されているケースが多くあります。この場合、社内システムにおける「統合認証」の要件として、これらを繋げてログインする必要が出てきます。これらの要求に対応するため、以下2点を確認し、Active Directory（以降AD）を中心とした統合認証を試してみます。</summary>
    <category term="認証認可" scheme="https://future-architect.github.io/categories/%E8%AA%8D%E8%A8%BC%E8%AA%8D%E5%8F%AF/"/>
    <category term="AD" scheme="https://future-architect.github.io/tags/AD/"/>
    <category term="Auth0" scheme="https://future-architect.github.io/tags/Auth0/"/>
    <category term="Auth0Rules" scheme="https://future-architect.github.io/tags/Auth0Rules/"/>
    <category term="SSO" scheme="https://future-architect.github.io/tags/SSO/"/>
    <category term="Salesforce" scheme="https://future-architect.github.io/tags/Salesforce/"/>
  </entry>
  <entry>
    <title>Auth0の出版記念に行ってきました！</title>
    <link href="https://future-architect.github.io/articles/20201124/"/>
    <id>https://future-architect.github.io/articles/20201124/</id>
    <published>2020-11-23T15:00:00.000Z</published>
    <updated>2020-11-23T15:00:00.000Z</updated>
    <author><name>山田勇一</name></author>
    <content type="html"><![CDATA[<h2 id="出版記念懇親会">出版記念懇親会</h2><p>Auth0さんより、クローズドの電子書籍出版記念にご招待いただき、Futureより3名で出席してまいりました。</p>
<ul>
<li>マンガでわかる！ Auth0誕生の秘密とは<br>公式ページ<br>ダウンロードページ</li>
</ul>
<h2 id="Auth0について">Auth0について</h2><p>Auth0は、認証・認可のサービスを提供するIDaaSの1つです。</p>
<p>開発ライブラリが豊富でカスタマイズ性がとても高く、何よりデベロッパーフレンドリーな部分が開発者としてとても好きな部分です。</p>
<p>弊社でも複数の案件で採用し、そのご縁でAuth0さんのイベントに登壇させていただいた他、当ブログでもいくつか記事を掲載していますので、是非ご覧ください。</p>
<h2 id="出版記念懇親会の流れ">出版記念懇親会の流れ</h2><h3 id="1-Auth0-SVP-Internationalスティーブン・リー・プルマンさん挨拶">1. Auth0 SVP Internationalスティーブン・リー・プルマンさん挨拶</h3><p>Auth0 SVP Internationalのスティーブン・リー・プルマンさんによるスピーチで、日本市場に対しての抱負が語られました。<br>写真掲載はAuth0さんに許可を頂いております。</p>
<p>快くご提供いただき、ありがとうございました！</p>
<img fetchpriority="high" src="/images/2020/20201124/Steven-Rees-Pullmanのコピー.jpg" width="600" height="900">

<ul>
<li>トピック<ul>
<li>Auth0として、日本語ローカライズにコミットすること</li>
<li>日本人スタッフを増やし、体制を強化すること</li>
<li>日本事業が好調で、2021年は大きな成長をめざしていること</li>
</ul>
</li>
</ul>
<p>利用していても、メニュー・サポートの日本語化はとてもニーズの高い内容だと感じているので、期待して待ちたいと思います。</p>
<h3 id="2-懇親会">2. 懇親会</h3><p>お酒を飲んでしまい、写真をほぼ取りそびれています。</p>
<p>辛うじて一緒に出席したメンバーが残した料理の写真です。</p>
<img src="/images/2020/20201124/iOS_の画像_(5).jpg" width="1200" height="1600" loading="lazy">

<h2 id="ノベルティ">ノベルティ</h2><ul>
<li>書籍版の漫画<br>大本命の書籍です。<br>Auth0の誕生秘話が語られています。</li>
<li>事例集<br>８つの事例が紹介してあり、最近のAuth0さんの勢いを感じる内容でした。</li>
<li>Auth0マスク</li>
<li>Auth0カレー<img src="/images/2020/20201124/iOS_の画像_(4).jpg" width="1200" height="1600" loading="lazy"></li>
</ul>
<h2 id="フォトジェニックスポット">フォトジェニックスポット</h2><p>Auth0のCEO&#x2F;CTOと写真の撮れるフォトジェニックスポットも用意されていました。<br>せっかくなので、出席メンバー個別に撮影。恐らく、一番はしゃいでた集団だったと思います。</p>
<p>※撮影時以外はマスクを着用しておりました。</p>
<img src="/images/2020/20201124/iOS_の画像_(2).jpg" width="1200" height="1600" loading="lazy">

<img src="/images/2020/20201124/iOS_の画像_(3).jpg" width="1200" height="1600" loading="lazy">

<p>どうも私です！</p>
<img src="/images/2020/20201124/iOS_の画像_(1).jpg" width="1200" height="900" loading="lazy">

<h2 id="なにはともあれ">なにはともあれ</h2><p>出版おめでとうございます！</p>
<p>フューチャー技術ブログのAuth0連載も盛り上げていきます。よろしくおねがいします。</p>
]]></content>
    <summary type="html">Auth0さんより、クローズドの電子書籍出版記念にご招待いただき、Futureより3名で出席してまいりました。- マンガでわかる！Auth0誕生の秘密とは...</summary>
    <category term="認証認可" scheme="https://future-architect.github.io/categories/%E8%AA%8D%E8%A8%BC%E8%AA%8D%E5%8F%AF/"/>
    <category term="Auth0" scheme="https://future-architect.github.io/tags/Auth0/"/>
    <category term="出版" scheme="https://future-architect.github.io/tags/%E5%87%BA%E7%89%88/"/>
  </entry>
  <entry>
    <title>Auth0の設定をバージョン管理し、Auth0 Deploy CLIを利用してデプロイ環境を整える</title>
    <link href="https://future-architect.github.io/articles/20200702/"/>
    <id>https://future-architect.github.io/articles/20200702/</id>
    <published>2020-07-02T00:48:34.000Z</published>
    <updated>2020-07-02T00:48:34.000Z</updated>
    <author><name>市川浩暉</name></author>
    <content type="html"><![CDATA[<h2 id="はじめに">はじめに</h2><p>こんにちは、TIG&#x2F;DXユニットの市川です。</p>
<p>私が所属しているプロジェクトでは認証認可基盤としてAuth0を使用しています。検証段階や初期構築段階では各種設定をダッシュボードから操作することが多いと思いますが、実際に本番運用を行っていると、Auth0の設定やRulesのスクリプトをGit管理し、変更履歴を追えるようにしたいというケースが出てくるかと思います。</p>
<p>今回は、Auth0から提供されているAuth0 Deploy CLIという拡張機能を利用して、Auth0テナントの設定をエクスポートする方法と私のプロジェクトで実際に行っているAuth0テナントへのデプロイの方法をお伝えします。</p>
<p>最終的なイメージは下記の通りです。</p>
<img fetchpriority="high" src="/images/2020/20200702/photo_20200702_01.png" width="971" height="575">

<h2 id="1-実行環境">1. 実行環境</h2><p>今回の記事の作成で使用した実行環境は以下の通りです。</p>
<p>npm：6.14.5<br>auth0-deploy-cli：5.0.0</p>
<h2 id="2-Auth0-Deploy-CLIとは">2. Auth0 Deploy CLIとは</h2><p>Auth0 Deploy CLIとはAuth0で提供されている拡張機能です。<br>これを利用するとAuth0テナントの設定情報をエクスポートできることに加え、テナントの設定情報を記載したファイルを、各Auth0テナントへ反映できます。<br>よって、この拡張機能をCI&#x2F;CDに組み込み、そこからデプロイを行うことも可能です。</p>
<h2 id="3-既存環境のエクスポート">3. 既存環境のエクスポート</h2><p>では早速既存環境のエクスポートから行っていきます。</p>
<h3 id="3-1-Auth0-Deploy-CLIで使用するアプリケーションを各テナントに作成する">3-1. Auth0 Deploy CLIで使用するアプリケーションを各テナントに作成する</h3><p>Auth0テナントにAuth0 Deploy CLIで使用するアプリケーションを作成します。<br>（各環境ごとにAuth0テナントを作成している場合は、それぞれのテナントごとにApplicationを作成する必要があります）</p>
<p>Application Type はM2M(Machine to Machine Applications)を指定します。</p>
<img src="/images/2020/20200702/photo_20200702_02.png" width="763" height="510" loading="lazy">

<p>使用するAPIはAuth0 Management APIを選択し、auth0-deploy-cliを使用するに当たり必要となるSCOPEを設定します。</p>
<p>必要となるSCOPEは下記をご確認ください。</p>
<p>Required Scopes - Create and Configure the Deploy CLI Application Manually</p>
<p>SCOPE設定後、ポップアップ下部のAUTHORIZEを押下します。</p>
<img src="/images/2020/20200702/photo_20200702_03.png" width="650" height="546" loading="lazy">

<h3 id="3-2-ローカル環境にAuth0-Deploy-CLIの拡張機能をインストールする。">3-2. ローカル環境にAuth0 Deploy CLIの拡張機能をインストールする。</h3><p>npmを使用して、auth0-deploy-cliをローカル環境にインストールします。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">npm i -g auth0-deploy-cli</span><br></pre></td></tr></table></figure>

<h3 id="3-3-エクスポート先のディレクトリの作成">3-3. エクスポート先のディレクトリの作成</h3><p>まずエクスポートする設定の置き場所となる任意のディレクトリを作成します。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line"><span class="built_in">mkdir</span> auth0-deploy</span><br><span class="line"><span class="built_in">cd</span> auth0-deploy</span><br></pre></td></tr></table></figure>

<h3 id="3-4-設定ファイルのconfig-jsonを作成">3-4 設定ファイルのconfig.jsonを作成</h3><p>次にエクスポートを行う際に使用するconfig.jsonを作成します。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line"><span class="built_in">touch</span> config.json</span><br></pre></td></tr></table></figure>

<p>作成したconfig.jsonに、先程作成したアプリケーションのdomainやclient id、client secrtetの情報を下記のフォーマットで記載します。</p>
<figure class="highlight json"><figcaption><span>auth0-deploy/config.json</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;AUTH0_DOMAIN&quot;</span><span class="punctuation">:</span> <span class="string">&quot;YOUR_DOMAIN&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;AUTH0_CLIENT_ID&quot;</span><span class="punctuation">:</span> <span class="string">&quot;YOUR_CLIENT_ID&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;AUTH0_CLIENT_SECRET&quot;</span><span class="punctuation">:</span> <span class="string">&quot;YOUR_CLIENT_SECRET&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure>

<p>client id等の情報はAuth0のダッシュボードから確認できます。</p>
<img src="/images/2020/20200702/photo_20200702_04.png" width="800" height="446" loading="lazy">

<h3 id="3-5-exportコマンドでエクスポートする">3-5. exportコマンドでエクスポートする</h3><p>これでテナントの設定をエクスポートする準備が整いました。<br>下記コマンドを実行して、Auth0テナントの設定をエクスポートします。</p>
<figure class="highlight sh"><table><tr><td class="code"><pre><span class="line">a0deploy <span class="built_in">export</span> -c config.json -f yaml -o ./</span><br></pre></td></tr></table></figure>

<p>-cはconfigファイル、-fはフォーマット、-oはエクスポート先のディレクトリを指定します。<br>プロキシ経由の場合はプロキシのオプション（-p）を設定します。</p>
<p>詳細はこちらをご確認ください。</p>
<p>コマンドを実行し、ログの最後に<code>Export Successful</code>が出力されれば、エクスポートは成功です。</p>
<div class="code-block"><figure class="highlight console"><input type="checkbox" id="code-wrap-znugsp-1" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-znugsp-1" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">20XX-YY-ZZ:ZZ:SS.SSSZ - info: Loading Auth0 Tenant Data</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">20XX-YY-ZZ:ZZ:SS.SSSZ - info: Retrieving rules data from Auth0</span></span><br><span class="line">...</span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">20XX-YY-ZZ:ZZ:SS.SSSZ - info: Exporting guardianFactorTemplates</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">20XX-YY-ZZ:ZZ:SS.SSSZ - info: Exporting roles</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">20XX-YY-ZZ:ZZ:SS.SSSZ - info: Writing tenant.yaml</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">20XX-YY-ZZ:ZZ:SS.SSSZ - info: Export Successful</span></span><br></pre></td></tr></table></figure></div>

<h3 id="3-6-ディレクトリ構成">3-6. ディレクトリ構成</h3><p>各環境が設定している内容により異なることはありますが、私が使用しているテナントは下記のようなディレクトリ構成となりました。</p>
<div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-znugsp-2" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-znugsp-2" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">|   <span class="comment"># 新規登録時やアカウントブロック時に送信されるemailテンプレート</span></span><br><span class="line">├── emailTemplates</span><br><span class="line">│   ├── blocked_account.html</span><br><span class="line">│   ├── reset_email.html</span><br><span class="line">│   └── verify_email.html</span><br><span class="line">|</span><br><span class="line">|   <span class="comment"># Universal Loginで使用するログイン画面やパスワード再発行ページ</span></span><br><span class="line">├── pages</span><br><span class="line">|   ├── error_page.html</span><br><span class="line">|   ├── login.html</span><br><span class="line">|   └── password_reset.html</span><br><span class="line">|</span><br><span class="line">|   <span class="comment"># テナントにされている各Rulesのスクリプト</span></span><br><span class="line">├── rules</span><br><span class="line">|   ├── hoge.js</span><br><span class="line">|   └── fuga.js</span><br><span class="line">|</span><br><span class="line">|   <span class="comment"># テナントの設定が記載されているyamlファイル</span></span><br><span class="line">└── tenant.yaml</span><br></pre></td></tr></table></figure></div>

<h3 id="3-7-各環境ごとのデプロイに対応したディレクトリ構成に変更する">3-7. 各環境ごとのデプロイに対応したディレクトリ構成に変更する</h3><p>私が所属しているプロジェクトでは3つの環境を使用しているため、Auth0テナントも3つ使用しています。<br>各環境ごとに設定内容が同一ではない項目もあるため、テナントの設定が記載されているyamlファイルを環境ごとに作成しています。<br>よって下記のようなディレクトリ構成となります。</p>
<div class="code-block"><figure class="highlight sh"><input type="checkbox" id="code-wrap-znugsp-3" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-znugsp-3" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line">|   <span class="comment"># 新規登録時やアカウントブロック時に送信されるemailテンプレート</span></span><br><span class="line">├── emailTemplates</span><br><span class="line">│   ├── blocked_account.html</span><br><span class="line">│   ├── reset_email.html</span><br><span class="line">│   └── verify_email.html</span><br><span class="line">|</span><br><span class="line">|   <span class="comment"># Universal Loginで使用するログイン画面やパスワード再発行ページ</span></span><br><span class="line">├── pages</span><br><span class="line">|   ├── error_page.html</span><br><span class="line">|   ├── login.html</span><br><span class="line">|   └── password_reset.html</span><br><span class="line">|</span><br><span class="line">|   <span class="comment"># テナントにされている各Rulesのスクリプト</span></span><br><span class="line">├── rules</span><br><span class="line">|   ├── hoge.js</span><br><span class="line">|   └── fuga.js</span><br><span class="line">|</span><br><span class="line">|   テナントの設定が記載されているyamlファイル</span><br><span class="line">└── tenant-dev.yaml</span><br><span class="line">└── tenant-stg.yaml</span><br><span class="line">└── tenant-prd.yaml</span><br></pre></td></tr></table></figure></div>

<h2 id="4-環境ごとに差異がある値を環境変数に定義する">4. 環境ごとに差異がある値を環境変数に定義する</h2><p>テナントの設定をエクスポートした時点ではドメイン等、環境によって変えたい変数がハードコーディングされている状態になっています。<br>もしrulesやemailTemplate配下のファイルで環境ごとにセットしたい値が異なる場合は、環境変数を用いて正しい値がセットされるようにします。</p>
<p>Auth0 Deploy CLIでは、デプロイ時に指定するconfigファイルに<code>AUTH0_KEYWORD_REPLACE_MAPPINGS</code> を指定して環境変数のセットを行うと、configファイルから値を読み取って環境変数がセットされます。</p>
<figure class="highlight json"><figcaption><span>auth0-deploy/config.json</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;AUTH0_DOMAIN&quot;</span><span class="punctuation">:</span> <span class="string">&quot;YOUR_DOMAIN&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;AUTH0_CLIENT_ID&quot;</span><span class="punctuation">:</span> <span class="string">&quot;YOUR_CLIENT_ID&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;AUTH0_CLIENT_SECRET&quot;</span><span class="punctuation">:</span> <span class="string">&quot;YOUR_CLIENT_SECRET&quot;</span><span class="punctuation">,</span></span><br><span class="line">   <span class="attr">&quot;AUTH0_KEYWORD_REPLACE_MAPPINGS&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">        <span class="attr">&quot;DOMAIN&quot;</span><span class="punctuation">:</span> <span class="string">&quot;https://www.example.com&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure>

<p>下記に例を記載します。</p>
<p>login.htmlが下記の様になっていた場合、</p>
<figure class="highlight html"><figcaption><span>auth0-source/pages/login.html</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">a</span> <span class="attr">href</span>=<span class="string">&quot;##DOMAIN##/sample&quot;</span>&gt;</span>サンプルリンク<span class="tag">&lt;/<span class="name">a</span>&gt;</span></span><br></pre></td></tr></table></figure>

<p>デプロイ時にはconfig.jsonファイルから環境変数が適用され、環境ごとに値を変えることができます。</p>
<div class="code-block"><figure class="highlight html"><input type="checkbox" id="code-wrap-znugsp-4" class="code-wrap-input code-wrap-narrow" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-znugsp-4" title="コードの折り返しを切り替える"></label><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="name">a</span> <span class="attr">href</span>=<span class="string">&quot;https://www.example.com/sample&quot;</span>&gt;</span>サンプルリンク<span class="tag">&lt;/<span class="name">a</span>&gt;</span></span><br></pre></td></tr></table></figure></div>

<h2 id="5-デプロイ環境の整備">5. デプロイ環境の整備</h2><p>私のプロジェクトではソースコードの管理をGitlabで行っており、上記作業完了後、リモートリポジトリにpushします。</p>
<p>その後、Gitlab CI&#x2F;CDを起動して上記テナントのディレクトリをS3へアップロードしたことをトリガーにCodePipelineが起動し、CodeBuild上でAuth0テナントのデプロイを行っています。</p>
<p>実際に使用しているbuildspec.ymlは下記の通りです。<br>client idやclient secretを指定するconfigファイルはAWSのParameter Storeから読みとった値をCodeBuild上でセットしています。</p>
<div class="code-block"><figure class="highlight yml"><input type="checkbox" id="code-wrap-znugsp-5" class="code-wrap-input" aria-label="コードの折り返しを切り替える"><label class="code-wrap-label" for="code-wrap-znugsp-5" title="コードの折り返しを切り替える"></label><figcaption><span>buildspec.yml</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="attr">version:</span> <span class="number">0.2</span></span><br><span class="line"><span class="attr">phases:</span></span><br><span class="line">  <span class="attr">pre_build:</span></span><br><span class="line">    <span class="attr">commands:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">npm</span> <span class="string">install</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">npm</span> <span class="string">i</span> <span class="string">-g</span> <span class="string">auth0-deploy-cli@5.0.0</span></span><br><span class="line">  <span class="attr">post_build:</span></span><br><span class="line">    <span class="attr">commands:</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">echo</span> <span class="string">creating</span> <span class="string">config</span> <span class="string">file</span> <span class="string">started</span></span><br><span class="line">      <span class="comment"># ↓リポジトリ上ではclient id等をハードコーディングしたconfig.jsonファイルは保持せず、config.jsonのテンプレートファイルのみ保持し、Parameter Storeの値をもとにファイルを新規作成しています。</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">envsubst</span> <span class="string">&lt;</span> <span class="string">config-template.json</span> <span class="string">&gt;</span> <span class="string">config.json</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">echo</span> <span class="string">finished</span> <span class="string">creating</span> <span class="string">config</span> <span class="string">file</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">cat</span> <span class="string">config.json</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">echo</span> <span class="string">auth0-deploy-cli</span> <span class="string">version</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">a0deploy</span> <span class="string">--version</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">echo</span> <span class="string">Auth0</span> <span class="string">Deploy</span> <span class="string">started</span></span><br><span class="line">      <span class="comment"># ↓ENVは環境変数から読み取っています</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">a0deploy</span> <span class="string">import</span> <span class="string">-c</span> <span class="string">config.json</span> <span class="string">-i</span> <span class="string">tenant-$&#123;ENV&#125;.yaml</span></span><br><span class="line">      <span class="bullet">-</span> <span class="string">echo</span> <span class="string">Auth0</span> <span class="string">Deploy</span> <span class="string">completed</span></span><br></pre></td></tr></table></figure></div>

<p>これでデプロイ環境を整備できました。</p>
<p>基本的には初期構築時のExportファイルを正として管理していますが、Auth0上で大きな設定変更が生じた際は念の為テナント設定のExportを行い、正管理ファイルに誤りがないかどうか、必要に応じて確認しています。</p>
<h2 id="6-最後に">6. 最後に</h2><p>Auth0 Deploy CLIを利用して既存テナントの設定をエクスポートするところから、実際にCI&#x2F;CDに組み込んでデプロイを行う部分までをご紹介してきました。<br>ただ、Auth0の設定管理は今回扱ったAuth0 Deploy CLIだけでなく、Teraformでも管理できます（https://www.terraform.io/docs/providers/auth0/index.html）</p>
<p>そのため、自身が所属しているプロジェクトの状況に応じて適切なものを選択・利用していくのが良いかと思います。</p>
<h2 id="7-関連する記事">7. 関連する記事</h2><ul>
<li>Auth0 Deploy CLI Tool</li>
<li>Auth0 導入編</li>
<li>Auth0 EmailまたはSMSを使ったパスワードレス認証を設定する</li>
<li>Auth0のRulesを使って認証認可を自在にカスタマイズする</li>
</ul>
]]></content>
    <summary type="html">私が所属しているプロジェクトでは認証認可基盤としてAuth0を使用しています。検証段階や初期構築段階では各種設定をダッシュボードから操作することが多いと思いますが、実際に本番運用を行っていると、Auth0の設定やRulesのスクリプトをGitで管理し、変更履歴を追えるようにしたいというケースが出てくるかと思います。</summary>
    <category term="認証認可" scheme="https://future-architect.github.io/categories/%E8%AA%8D%E8%A8%BC%E8%AA%8D%E5%8F%AF/"/>
    <category term="AWS" scheme="https://future-architect.github.io/tags/AWS/"/>
    <category term="Auth0" scheme="https://future-architect.github.io/tags/Auth0/"/>
    <category term="CLI" scheme="https://future-architect.github.io/tags/CLI/"/>
    <category term="GitLab" scheme="https://future-architect.github.io/tags/GitLab/"/>
    <category term="バージョン管理" scheme="https://future-architect.github.io/tags/%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B3%E7%AE%A1%E7%90%86/"/>
  </entry>
</feed>
