ちゃなべの備忘録

ほぼ備忘録です。

RSCとかがわからなかったから完全に理解するまで【備忘録】

はじめに

panda CSSを触ってみたんですが、サーバー側でbuildするので、クライアント側での変更をいちいちCSSに反映するのが難しそうに思える。

まぁぼくの調査不足だと思うので、一旦pandaCSSを調査することにした。
その時に出てきたのが、「RSC互換性」というワードであり、いわゆるReact Server Components との互換性のことである。

「いや、、RSCよく知らんな」ということで調査します。

調査

まっさきにこの記事が出てきたので読んでまとめる。

zenn.dev

  • RSCはReactコンポーネントをサーバーサイドでレンダリングする技術
  • もちろんClientでのレンダリングも両立が可能なので、部分的に処理を早くしたりなどができる
  • また、React Server ComponentsはConcurrent Modeに完全対応し、それを前提として動作します。
  • RSCはコンポーネントを三種類に分類します
  • Server Components
    • サーバーのみでレンダリングされるコンポーネント
    • 拡張子を.server.jsとするとServer Componentsとして解釈されます
    • 以下の制約を持ちます
      • ステートを持てないため、useStateuseReducerは使えない
      • リレンダーが走らないので、useEffectuseLayoutEffectは使えない
      • クライアントとは分離された環境でレンダーされるため、useContextは使えない
      • ブラウザのみ利用可能なAPI (DOMやWebAPI)は使えない
    • Server ComponentsはClient Componentsを子コンポーネントに持つことができます
  • Client Components
    • 従来のクライアントのみでレンダリングされるコンポーネントのこと
    • 従来のコンポーネントですが、拡張子を.client.jsとする必要がある
    • ステートを持ったり、イベントをハンドルしたり、WebAPIを利用したりする必要があるときに利用
    • Server Componentsをimportすることはできません
      • Server componentsのレンダーに必要なサーバーへのリクエストのせいでのパフォーマンス低下を防ぐため
    • childrenなどのpropを通じてServer ComponentsをClient Componentsに渡すことはできる
      • これはServer ComponentsをレンダリングしたJSXをpropsに渡すだけなので、ただのJSXを渡すことに等しい
  • Shared Components
    • サーバーとクライアントのどちらでもレンダリング可能なコンポーネントのこと
    • Server Componentsから呼ばれた場合はServer Componentsとして、Client Componentsから呼ばれた場合はClient Componentsとして振舞います
    • 拡張子は.js
    • 両方の制約を持ちます。ステートは持てずで、Server Componentsをimportすることもできません
  • 筆者が考えるベストプラクティスは以下
    • 基本的にパフォーマンス有利なServer Componentsにする
    • ステートが必要だったり、イベントをハンドルしたかったりするときはClient Components
    • 両方で使いたいときはShared Componentsに。
  • RSCの恩恵はパフォーマンス面が主
    • バンドルサイズの減少
      • Server Componentsのコードはクライアントがダウンロードするバンドルに含まれない
      • そのため、ユーザーのインタラクションに反応しない大部分のコンポーネントを、バンドルから取り除くことができる
      • これによって
        • 通信にかかるコストの減少
        • コードのパースにかかるコストを減少
        • Virtual DOMにマウントされるコンポーネントが減少 → メモリの使用量が減少
    • データフェッチにかかる時間の減少
      • Server Componentsはサーバー上で実行されるため、データに直接アクセスできることがあり、レイテンシーを大きく抑えられる
      • 一つのデータフェッチが完了した後に別のフェッチが開始するようなWaterfallの影響が小さくなります
    • Code Splittingの自動化
      • Server Componentsのレンダリングが終了した時点で、必要となるClient Componentsがどれかというのが判明します
      • RSCでは、自動でClient Componentsを切り分けてバンドル化し、必要となるもののみをクライアントに指定してダウンロードさせる
    • レンダー結果のDOMへの更新は最小限
      • Server Componentsが返すレンダリング結果はHTMLではなくReactコンポーネントです
      • そのため、自然にVirtual DOMにマージすることが可能
      • 変更されたDOM要素のみが更新され、マウント済みのClient Componentsの状態が失われることも、更新されなかったDOMの状態も失われない
  • SSRとは別技術であり、共存が可能です

Concurrent Modeが何かわからん、調べる。

www.ey-office.com

  • Concurrent Modeは日本語でいうと「並列モード」
  • 背景
    • JSはシングルスレッドですが、イベント駆動モデルです。イベントがあれば、進んでいた処理(画面表示など)を終わるのを待たずにそちらの処理に切り替えることができます。これによって並列であるかのように感じます
    • しかし大量の処理があるとクリックなどのイベントが反応できなくなり、UXが悪くなります
    • 仮想DOMは特に重く、React15.x まではレンダリングが終わるまでイベント等は反応しませんでした(ブロッキングレンダリング)
    • React16.0はレンダリングエンジンがReact Fiberに変わり、中断や切り替え可能な細かい単位でレンダリングを行えるようになりました
  • TransitionやDefferredValueが正式な並列ができるAPIとしてリリースされました

zenn.dev

  • Concurrent Reactは機能ではなく、新しいメカニズム
  • Suspense
    • 内部のコンポーネントがロード中でまだレンダリングできない状態を処理できる
    • 表示されるまで「ぐるぐる(Spinner)」を表示するとかね
    • だが、現状のサポートはReact.lazyくらい
    • useQuery などで、データを読み込む時にSuspenseを用いれば簡略化して記述可能。だけどまだ非推奨。

ブラウザ拡張機能フレームワークのPlasmoを使ってみた【備忘録】

はじめに

chrome拡張機能を作ろうと思ったんだけど、手動でやるといろいろ面倒だったので、こないだリリースしたらしいフレームワーク Plasmo を使ってみた。それをまとめる。

www.plasmo.com

やってみる

もうさっそくやっちゃうよ。

Plasmoをinit

# フレームワークの作成
$ pnpm create plasmo
.../Library/pnpm/store/v3/tmp/dlx-88105  |   +2 +
Packages are hard linked from the content-addressable store to the virtual store.
  Content-addressable store is at: /Users/ayumu-1212/Library/pnpm/store/v3
  Virtual store is at:             ../../Library/pnpm/store/v3/tmp/dlx-88105/node_modules/.pnpm
.../Library/pnpm/store/v3/tmp/dlx-88105  | Progress: resolved 2, reused 0, downloaded 2, added 2, done
🟣 Plasmo v0.80.0
🔴 The Browser Extension Framework
🟡 Extension name: auto-diary
🟡 Extension description: An extension that automatically measures the engineer's daily progress.
🟡 Author name: ayumuabe1434@gmail.com
🔵 INFO   | Creating new project with popup
🔵 INFO   | Installing dependencies...
Downloading registry.npmjs.org/typescript/5.1.6: 7.15 MB/7.15 MB, done
Downloading registry.npmjs.org/@parcel/transformer-js/2.9.3: 45.7 MB/45.7 MB, done
 WARN  deprecated stable@0.1.8: Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility
Downloading registry.npmjs.org/@swc/core-darwin-arm64/1.3.69: 12.8 MB/12.8 MB, done
Downloading registry.npmjs.org/@swc/core-darwin-arm64/1.3.66: 12.1 MB/12.1 MB, done
Packages: +544
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Packages are hard linked from the content-addressable store to the virtual store.
  Content-addressable store is at: /Users/ayumu-1212/Library/pnpm/store/v3
  Virtual store is at:             node_modules/.pnpm
node_modules/.pnpm/@parcel+watcher@2.1.0/node_modules/@parcel/watcher: Running install script, done in 785ms
node_modules/.pnpm/sharp@0.32.1/node_modules/sharp: Running install script, done in 4.6s
Progress: resolved 607, reused 1, downloaded 543, added 544, done
node_modules/.pnpm/@swc+core@1.3.66/node_modules/@swc/core: Running postinstall script, done in 345ms
node_modules/.pnpm/@swc+core@1.3.69/node_modules/@swc/core: Running postinstall script, done in 816ms
node_modules/.pnpm/esbuild@0.18.13/node_modules/esbuild: Running postinstall script, done in 1s
node_modules/.pnpm/msgpackr-extract@3.0.2/node_modules/msgpackr-extract: Running install script, done in 1.2s
node_modules/.pnpm/lmdb@2.7.11/node_modules/lmdb: Running install script, done in 1s

dependencies:
+ plasmo 0.80.0
+ react 18.2.0
+ react-dom 18.2.0

devDependencies:
+ @plasmohq/prettier-plugin-sort-imports 4.0.1
+ @types/chrome 0.0.241
+ @types/node 20.4.2
+ @types/react 18.2.15
+ @types/react-dom 18.2.7
+ prettier 3.0.0
+ typescript 5.1.6

 WARN  Issues with peer dependencies found
.
└─┬ plasmo 0.80.0
  └─┬ @plasmohq/parcel-config 0.38.4
    └─┬ @parcel/config-default 2.9.3
      └─┬ @parcel/optimizer-htmlnano 2.9.3
        └─┬ htmlnano 2.0.4
          └── ✕ unmet peer svgo@^3.0.2: found 2.8.0 in @parcel/optimizer-htmlnano

Done in 34.2s
🔵 INFO   | Initializing git project...
🟢 DONE   | Your extension is ready in:  /Users/ayumu-1212/Documents/develop/auto-diary

    To start hacking, run:

       cd auto-diary
       pnpm dev

    Visit https://docs.plasmo.com for documentation and more examples.

おーーできた。階層構造は以下。

.
├── README.md
├── assets
│   └── icon.png
├── node_modules/
├── package.json
├── pnpm-lock.yaml
├── popup.tsx
└── tsconfig.json

buildして、公開するbuildファイルを作ってみる。

$ pnpm build

> auto-diary@0.0.1 build /Users/ayumu-1212/Documents/develop/auto-diary
> plasmo build

🟣 Plasmo v0.80.0
🔴 The Browser Extension Framework
🔵 INFO   | Prepare to bundle the extension...
🔵 INFO   | Loaded environment variables from: []
🟢 DONE   | Finished in 849ms!

$ tree
.
├── README.md
├── assets
│   └── icon.png
├── build
│   ├── chrome-mv3-dev
│   │   ├── icon128.plasmo.c11f39af.png
│   │   ├── icon16.plasmo.9f44d99c.png
│   │   ├── icon32.plasmo.83dbbbab.png
│   │   ├── icon48.plasmo.a78c509e.png
│   │   ├── icon64.plasmo.15206795.png
│   │   ├── manifest.json
│   │   ├── plasmo-default-background.e198ef58.js
│   │   ├── popup.7d3dc21e.js
│   │   └── popup.html
│   └── chrome-mv3-prod
│       ├── icon128.plasmo.3c1ed2d2.png
│       ├── icon16.plasmo.6c567d50.png
│       ├── icon32.plasmo.76b92899.png
│       ├── icon48.plasmo.aced7582.png
│       ├── icon64.plasmo.8bb5e6e0.png
│       ├── manifest.json
│       ├── popup.100f6462.js
│       └── popup.html
├── node_modules/
├── package.json
├── pnpm-lock.yaml
├── popup.tsx
└── tsconfig.json

これでbuildができたね。公開してみよう。

拡張機能を早速表示

いったん表示までしてみよう。ぼくはbraveを使っているのでここをブラウザで開く。

brave://extensions/

そして右上の デベロッパーモード をオンにする。

そして、[パッケージ化されていない拡張機能を読み込む] を押し、 build/chrome-mv3-dev を選択する。

すると、さっそく読み込まれて表示されている!!

chromeの履歴を取得できるようにしてみる。

最初の立ち上げ時はコードはこのようになっている。

import { useState } from "react"

function IndexPopup() {
  const [data, setData] = useState("")

  return (
    <div
      style={{
        display: "flex",
        flexDirection: "column",
        padding: 16
      }}>
      <h2>
        Welcome to your
        <a href="https://www.plasmo.com" target="_blank">
          {" "}
          Plasmo
        </a>{" "}
        Extension!
      </h2>
      <input onChange={(e) => setData(e.target.value)} value={data} />
      <a href="https://docs.plasmo.com" target="_blank">
        View Docs
      </a>
    </div>
  )
}

export default IndexPopup

まずは、history パーミッションmanifest.jsonに設定しないといけない。 直接書き込むのではなく、package.jsonに以下を追記する。

  "manifest": {
    "host_permissions": [
      "https://*/*"
    ],
+    "permissions": [
+      "history"
+    ]
  }

こうすることで、manifest.json に自動的に反映された。

popup.tsx を編集する。

import { useState } from "react"

function IndexPopup() {
+  const [historyItems, setHistoryItems] = useState<chrome.history.HistoryItem[]>([])
+
+  const getHistory = async() => {
+    const now = new Date()
+
+    const text = ""
+    const startTime = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1, 0, 0, 0).getTime()
+    const endTime = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0).getTime()
+    const maxResults = 10
+    const items = await chrome.history.search({
+      text,
+      startTime,
+      endTime,
+      maxResults,
+    })
+    setHistoryItems(items)
+  }

  return (
    <div
      style={{
        display: "flex",
        flexDirection: "column",
        padding: 16
      }}>
      <h2>
        Welcome to your
        <a href="https://www.plasmo.com" target="_blank">
          {" "}
          Plasmo
        </a>{" "}
        Extension!
      </h2>
+      <button id="getHistory" onClick={getHistory}>getHistory</button>
+      <div>
+        <ul>
+          {historyItems.map((item) => (<li id={item.id} key={item.id}>{item.title}</li>))}
+        </ul>
+      </div>
    </div>
  )
}

export default IndexPopup

いいねぇ、ボタンを押したら検索履歴が表示できるようになった。

とりあえずここまで!

GithubのOIDCのチュートリアルをやってみた【備忘録】

はじめに

GithubのOIDCを実装したかったんだけど、ちょっとチュートリアルを見つけたのでそれをパットやってみる。

今回やるのはこれ

docs.github.com

実装

GitHubAppの登録

まずはGithubで準備することがある。

docs.github.com

  1. GithubAppsの設定ページにアクセス
  2. [New GitHub App] をクリック
  3. 認証をする
  4. 必要な情報を入力する
    • GitHub App name →oidc-tutorial
    • 説明文 → 適当に
    • Homepage URL → GithubのレポジトリURL
    • Active → 解除
  5. 作成完了

Rubyの開発環境整え

参考はこちら

qiita.com

まずは以下のファイルを準備

  1. .gitignore
  2. Gemfile
  3. Gemfile.lock
  4. docker-compose.yaml
  5. Dockerfile
  6. app.rb
コマンドで1.2.3.のファイルを準備
# gitignoreをダウンロード
$ curl https://raw.githubusercontent.com/github/gitignore/master/Ruby.gitignore -o .gitignore

# GemfileとGemfile.lockを生成とダウンロード
$ docker run --rm --volume $(pwd):/app --workdir /app ruby:2.7-slim bundle init
$ docker run --rm --volume $(pwd):/app --workdir /app ruby:2.7-slim bundle add sinatra 
$ docker run --rm --volume $(pwd):/app --workdir /app ruby:2.7-slim bundle add dotenv
手動でDockerfileとdocker-compose.yamlを作成

ruby/appディレクトリを使うのが一般的なのか?まぁとりあえず従いますよ。

FROM ruby:2.7-slim
WORKDIR /app

COPY Gemfile ./
COPY Gemfile.lock ./
RUN bundle config --local set path 'vendor/bundle'
RUN bundle install

CMD bundle exec ruby app.rb
version: '3'
services:
  app:
    build: .
    volumes:
      - .:/app
      - /app/vendor/bundle
    ports:
      - 4567:4567
app.rb (アプリ自体のファイル) を作成

まぁ作成

require 'sinatra'

configure do
  set :bind, '0.0.0.0'
end

get '/' do
  'Hello Sinatra!'
end
起動

まぁ、ついたね。

OIDCの設定を進める

  1. GitHub Appsの設定ページを開く
  2. [Edit] を押す
  3. [Generate a new client secret] で新しいクライアントシークレットを発行する。
  4. クライアントIDと発行されたクライアントシークレットを.envに保存する
  5. gitignoreに.envを追加
CLIENT_ID="YOUR_CLIENT_ID"
CLIENT_SECRET="YOUR_CLIENT_SECRET"
- # .env
+ .env

ログインボタンを設置

app.rbに以下の記述をして、Loginボタンを設置します。

require "sinatra"
require "dotenv/load"
require "net/http"
require "json"

CLIENT_ID = ENV.fetch("CLIENT_ID")
CLIENT_SECRET = ENV.fetch("CLIENT_SECRET")

configure do
  set :bind, '0.0.0.0'
end

get "/" do
  link = '<a href="https://github.com/login/oauth/authorize?client_id=<%= CLIENT_ID %>">Login with GitHub</a>'
  erb link
end

linkをクリックすると、Callback先を指定してないので、こうなる。

Callbackを設定

sinatraでCallbackURLを設定

以下を追記

+  get "/github/callback" do
+    code = params["code"]
+    render = "Successfully authorized! Got code #{code}."
+    erb render
+  end
Githubに設定

GitHubAppの設定編集に行き、[Callback URL]を編集。

僕の場合は、http://localhost:4567/github/callback と入力

保存も忘れずに

やってみる

いいね、ちゃんとcodeも受け取れる。

access_tokenを受け取る

先程もらった、codeをもとに、access_tokenをもらいにいこう。

以下を追記した。

追記した内容は、

  • parse_response 関数で、GitHub API からの応答を解析します。
  • exchange_code 関数で、code パラメーターをユーザー アクセス トークンと交換します。
  • コールバック URL 要求のハンドラーで exchange_code を呼び出して、code パラメーターをユーザー アクセス トークンと交換するようになります。
  • コールバック ページに、トークンが生成されたことを示すテキストが表示されるようになります。 トークン生成が成功しなかった場合は、ページにそのエラーが示されます。
def parse_response(response)
  case response
  when Net::HTTPOK
    JSON.parse(response.body)
  else
    puts response
    puts response.body
    {}
  end
end

def exchange_code(code)
  params = {
    "client_id" => CLIENT_ID,
    "client_secret" => CLIENT_SECRET,
    "code" => code
  }
  result = Net::HTTP.post(
    URI("https://github.com/login/oauth/access_token"),
    URI.encode_www_form(params),
    {"Accept" => "application/json"}
  )

  parse_response(result)
end

get "/github/callback" do
  code = params["code"]

  token_data = exchange_code(code)

  if token_data.key?("access_token")
    token = token_data["access_token"]

    render = "Successfully authorized! Got code #{code} and exchanged it for a user access token ending in #{token[-9..-1]}."
    erb render
  else
    render = "Authorized, but unable to exchange code #{code} for token."
    erb render
  end
end

じゃあこれでやってみると、ちゃんと受け取れる!

access_tokenを使って、REST APIを叩く。

じゃあついにaccess_tokenを得ることができたので、REST APIを叩いてみよう。

Githubの設定変更

いつものようにGithubAppsを変更する。

  1. GithubAppsの設定ページにいき、[Edit]を押す
  2. 左のサイドバーの[Permissions & events]をクリック
  3. Repository permissionsの AdministrationContents をRead-onlyに変更する
  4. 保存
app.rbの修正

以下を追記して、修正

def get_repos(token)
  uri = URI("https://api.github.com/user/repos")

  result = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
    body = {"access_token" => token}.to_json

    auth = "Bearer #{token}"
    headers = {"Accept" => "application/json", "Content-Type" => "application/json", "Authorization" => auth}

    http.send_request("GET", uri.path, body, headers)
  end

  parse_response(result)
end

get "/github/callback" do
  code = params["code"]

  token_data = exchange_code(code)

  if token_data.key?("access_token")
    token = token_data["access_token"]

    repos = get_repos(token)

    render = "Successfully authorized! Welcome, #{repos})."
    erb render
  else
    render = "Authorized, but unable to exchange code #{code} for token."
    erb render
  end
end

すると、callbackページで、レポジトリの一覧を取得することができるようになった。

だけど、これ、privateレポジトリの取得ができないっぽい? どうやら、このappをinstallしたレポジトリしかprivateに関しては取得できないらしい。

えーーーっと、やりたいことと違ったかな。?

次やりたいこと

  • 静的Access Tokenを発行して、それだとprivateレポジトリの中身も見れるのか確認。
  • GitHub OAuthはどのように使えるのか調査

第1回 8時間ハッカソン【備忘録】

はじめに

8時間ハッカソンとは?

今回のテーマ

認証機能を自作しよう! OAuth, OIDCなど

使用言語

Go

11:30

開始。 じゃあ、まずは方針を立てよう。

認証機能っていっても何やる?笑

とりあえず調査

調査

ayumu1212.hatenablog.com

ということで、今回はGoを使って、OIDCサーバーを作ってみよう。もし余裕があったらOAuthも。

12:30

実装の方針を組み立てよう

調査

ayumu1212.hatenablog.com

ねぇ!(怒) 要件多すぎ!!

18:30

泣きそうww

設計

じゃあ設計。

まじこれだけ。

認可サーバーはGoで作って、アプリケーションはNext.jsで作ろうか。

Dockerで環境作った

version: '3'

services:
  client:
    build: ./services/client/
    volumes:
      - ./services/client/src/:/usr/local/src/
    working_dir: /usr/local/src
    tty: true
    container_name: manual-oidc_client
    ports:
      - 3000:3000

  oidc:
    build: ./services/oidc/
    volumes:
      - ./services/oidc/src/:/usr/local/src/
    working_dir: /usr/local/src
    tty: true
    container_name: manual-oidc_oidc
    ports:
      - 8080:80

ぜんっぜん間に合いませんでした。てか間に合わせる気がほぼなかった。。 途中でOIDCの勉強を始めたときにやること多すぎて萎えちゃった。。

第2回のときにこの続きやろうか。

Githubのリモートレポジトリをローカルから作る【備忘録】

はじめに

毎回Githubのサイトでリモートレポジトリを作ってたけど、そろそろだるい。。

なんかやり方ないかな?と思ったらあったから設定する

設定

まずGitHub CLIをinstallする

cli.github.com

$ brew install gh

そしたら、新しいレポジトリをlocalで作って、それをリモートレポジトリに反映してみよう。

# GitHubCLIにログイン&権限を与える
$ gh auth login
? What account do you want to log into? GitHub.com
? What is your preferred protocol for Git operations? HTTPS
? Authenticate Git with your GitHub credentials? Yes
? How would you like to authenticate GitHub CLI? Paste an authentication token
Tip: you can generate a Personal Access Token here https://github.com/settings/tokens
The minimum required scopes are 'repo', 'read:org', 'workflow'.
? Paste your authentication token: ****************************************
- gh config set -h github.com git_protocol https
✓ Configured git protocol
✓ Logged in as ayumu-1212

# GitHubのリモートレポジトリ作成
$ gh repo create manual-oidc --public

# いま作ったレポジトリをclone
$ gh repo clone manual-oidc

$ cd manual-oidc

$ git commit --allow-empty -m ":tada: first commit"

$ git push origin main

さぁこれでどうだ?

おおーーー!できた。今後が楽だな。

認証系について調べてみた (仕組み編)【備忘録】

はじめに

前回認証系のイメージ(概要)について調べた。

ayumu1212.hatenablog.com

「じゃあこれで実装できるね!」とはならない。
実際のOIDCの仕組みなどを理解しないことには始まらない。

今回はそれを調べる

調査

OpenID Connectのざっくりフロー

qiita.com

  • 実装にはサービスサーバーとIdPサーバーが必要っぽいな。
  • 認証の暗号化とか難しいことは抜きにして、以下のフローで実装するっぽい
    1. クライアントがサービスに「外部IdPでログインしたい」と伝える
    2. サービスがIdPサーバーへのリダイレクトURLをクライアントに伝えて、リダイレクトさせる
    3. リダイレクト先のIdPサーバーが提示した認証と認可の画面でクライアントは許可する
    4. 許可されたIdPはサービスへのリダイレクトURLと認可コードを与えて、リダイレクトさせる
    5. 認可コードを受け取ったサービスはIdPサーバーに認可コードを伝える
    6. IdPサーバーがアクセストークンとIDトークンをサービスに返す
    7. このIDトークンを検証するために、IdPサーバーにアクセスして、鍵(クライアントシークレットor公開鍵)を入手する
    8. 検証ができたら、「IdPによりユーザーが認証された」ことを確認できる

OAuthのざっくりフロー

qiita.com

  • 「ユーザー情報」を教えてくれるAPIがあるとする
  • もし誰にでもユーザー情報を教えてくれるAPIだったら大変だ
  • だから「悪意のないユーザー」であることを伝える必要がある
  • そのために アクセストーク がある。「悪意がないよ!」と伝えるもの。
  • そしてそのアクセストークンを発行するための 認可サーバー も必要。
  • アクセストークンを発行する条件はユーザー認証
  • このアクセストークンを発行する流れのプロトコルがOAuth

OpenID Connectのざっくりフロー (Part2)

qiita.com

  • さっきのOAuthのフローとほぼ一緒
  • だけど、発行するのは「アクセストークン」ではなく「IDトークン」
  • つまりOIDCはOAuthを拡張したもの
  • じゃあIDトークンってなに?
  • ざっくりいうと、「ユーザーが認証されたという事実とそのユーザーの属性情報を、捏造されていないことを確認可能な方法で、各所に引き回すためにあるトークン」

IDトークンってなに?

qiita.com

  • IDトークンの外観
    • IDトークンは以下のような文字列。ピリオドで3つに別れている。
eyJraWQiOiIxZTlnZGs3IiwiYWxnIjoiUlMyNTYifQ.ewogImlz
cyI6ICJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4
Mjg5NzYxMDAxIiwKICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAi
bi0wUzZfV3pBMk1qIiwKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEz
MTEyODA5NzAsCiAibmFtZSI6ICJKYW5lIERvZSIsCiAiZ2l2ZW5fbmFtZSI6
ICJKYW5lIiwKICJmYW1pbHlfbmFtZSI6ICJEb2UiLAogImdlbmRlciI6ICJm
ZW1hbGUiLAogImJpcnRoZGF0ZSI6ICIwMDAwLTEwLTMxIiwKICJlbWFpbCI6
ICJqYW5lZG9lQGV4YW1wbGUuY29tIiwKICJwaWN0dXJlIjogImh0dHA6Ly9l
eGFtcGxlLmNvbS9qYW5lZG9lL21lLmpwZyIKfQ.rHQjEmBqn9Jre0OLykYNn
spA10Qql2rvx4FsD00jwlB0Sym4NzpgvPKsDjn_wMkHxcp6CilPcoKrWHcip
R2iAjzLvDNAReF97zoJqq880ZD1bwY82JDauCXELVR9O6_B0w3K-E7yM2mac
AAgNCUwtik6SjoSUZRcf-O5lygIyLENx882p6MtmwaL1hd6qn5RZOQ0TLrOY
u0532g9Exxcm-ChymrB4xLykpDj3lUivJt63eEGGN6DH5K6o33TcxkIjNrCD
4XB1CKKumZvCedgHHF3IAK4dVEDSUoGlH9z4pP_eWYNXvqQOjGs-rDaQzUHl
6cQQWNiDpWOl_lxXjQEvQ
  • 「ヘッダー.ペイロード.署名」というふうに別れている
    • この形式はJWS ( JSON Web Signature ) で定義されている
  • この3つの部分はそれぞれbase64urlでエンコードされたもの。つまりデコードすると内容がわかる。
{"kid":"1e9gdk7","alg":"RS256"}
{
 "iss": "http://server.example.com",
 "sub": "248289761001",
 "aud": "s6BhdRkqt3",
 "nonce": "n-0S6_WzA2Mj",
 "exp": 1311281970,
 "iat": 1311280970,
 "name": "Jane Doe",
 "given_name": "Jane",
 "family_name": "Doe",
 "gender": "female",
 "birthdate": "0000-10-31",
 "email": "janedoe@example.com",
 "picture": "http://example.com/janedoe/me.jpg"
}
 239 191 189 116  35  18  96 106 239 191 189 239 191 189 107 123
  67 239 191 189 239 191 189  70  13 239 191 189 239 191 189  64
 239 191 189  68  42 239 191 189 106 239 191 189 199 129 108  15
  77  35 239 191 189  80 116  75  41 239 191 189  55  58  96 239
 191 189 239 191 189 239 191 189  14  57 239 191 189 239 191 189
 239 191 189   7 239 191 189 239 191 189 122  10  41  79 114 239
 191 189 239 191 189  88 119  34 239 191 189  29 239 191 189   2
  60 203 188  51  64  69 239 191 189 125 239 191 189  58   9 239
 191 189 239 191 189  60 209 144 239 191 189 111   6  60 216 144
 218 184  37 239 191 189  45  84 125  59 239 191 189 239 191 189
 239 191 189  13 239 191 189 239 191 189  78 239 191 189  51 105
 239 191 189 112   0  32  52  37  48 239 191 189  41  58  74  58
  18  81 239 191 189  92 127 227 185 151  40   8 200 177  13 239
 191 189 239 191 189  54 239 191 189 239 191 189  45 239 191 189
   6 239 191 189 239 191 189  23 122 239 191 189 126  81 100 239
 191 189  52  76 239 191 189 239 191 189  98 239 191 189  57 239
 191 189 104  61  19  28  92 239 191 189 239 191 189 239 191 189
 239 191 189 106 239 191 189 239 191 189  18 239 191 189 239 191
 189 239 191 189 239 191 189 239 191 189  85  34 239 191 189 239
 191 189 122 239 191 189 239 191 189   6  24 222 131  31 239 191
 189 239 191 189 239 191 189 125 239 191 189 115  25   8 239 191
 189 239 191 189 239 191 189  15 239 191 189 239 191 189 239 191
 189  34 239 191 189 239 191 189 102 111   9 239 191 189  96  28
 113 119  32   2 239 191 189 117  81   3  73  74   6 239 191 189
 127 115 239 191 189 239 191 189 239 191 189 121 102  13  94 239
 191 189 239 191 189  58  49 239 191 189 239 191 189 239 191 189
 239 191 189  67  53   7 239 191 189 239 191 189  16  65  99  98
  14 239 191 189 239 191 189 239 191 189 239 191 189 113  94  52
   4 239 191 189  10
  • 3つめの署名は1つめのヘッダーのalgの値によって暗号化方法が異なる。
  • この署名が正しさを証明したいのは、「ヘッダー」と「ペイロード」の2つ
  • 署名がない「ヘッダー.ペイロード.」という形式の Unsecured JWS というのもある

  • IDトークンには「ヘッダー.キー.初期ベクター.暗号文.認証タグ」という5つに別れた形式もある

    • JWE (JSON Web Encryption) で定義している形式
  • これはIDトークンを暗号化したいときに利用される
  • OIDCの文脈だと4つめの暗号文の中に、「ヘッダー.ペイロード.署名」(つまりJWS)が入っている。
  • 2段階に暗号化を行っている
    • 対称性鍵暗号の共有鍵を生成する
    • 平文を共有鍵で暗号化する
    • 共有鍵を非対称鍵暗号の公開鍵で暗号化する
    • 暗号化した平文と共有鍵を復号化する人に渡す
    • 共有鍵を手元の秘密鍵で復号する
    • 復号した共有鍵で暗号化した平文を復号化する
  • 上記のフローに必要な情報はそれぞれ以下のように送られる
    • 暗号化された平文:JWE形式の4つめ
    • 平文を暗号化した共有鍵の形式:JWE形式の1つめのヘッダー内の enc keyのvalue
    • 暗号化された共有鍵:JWE形式の2つめ
    • 共有鍵を暗号化した形式:JWE形式の1つめのヘッダー内の alg keyのvalue
  • では、非対称鍵暗号を使う場合は公開鍵を暗号化する側に渡さなければいけません。
  • その仕様がJWK ( JSON Web Key ) です。

    • 使用はJWK Set Documentにかかれている
      • JWKの集合が書かれている
      • どう使うか、何に使ったほうがいいのか、などが書かれている
      • これは、JWKの公開鍵をまとめたもの
    • 使い方
      1. 最終的にIDトークンやアクセストークンを受け取ったサービス側がIDトークンやアクセストークンを検証するために、公開されているJWKを用いる
      2. 共有鍵を暗号化するときに、JWK Set Documentを見て、公開鍵を取得して、共有鍵を暗号化する
    • JWK Set Documentのありかの伝え方
      1. クライアントアプリの属性 jwks の値としてJWK Set Document自体が登録されている
      2. クライアントアプリの属性 jwks_uri の値としてJWK Set Documentが置かれている場所が登録されている
  • いままで説明してきた、暗号化する平文 ( JWSのペイロードや、JWEの暗号文など ) にも仕様があります。

  • それが JWT ( JSON Web Token ) です。
    • 定義:JWT とは、JSON 形式で表現されたクレーム (claim) の集合を、JWS もしくは JWE に埋め込んだもの
    • また、IDトークンの文脈におけるJWEは、「JWEの中にJWSがある」と言いましたね?ということはJWTがネストされているわけです。
    • このコトをNested JWTといいます。
    • JWTはクレームの集合なのですが、形式的には以下のようにキー・バリューのペアとして表現されています。
{
    "クレーム名": クレーム値,
    "クレーム名": クレーム値,
    ......
}
  • ここまで話してきた内容を踏まえると、 「暗号化されたIDトークンは『JWSがJWEに含まれている形のNested JWT」 です
  • 最後にIDトークンのクレームを見ていきましょう
    • iss クレーム
      • iss クレームは、JWT の発行者 (issuer) を識別するための識別子です
      • valueは文字列かURI
      • この値は他者の識別子との衝突を避けるために、自分の管理化にあるドメイン名のURLとすべき
      • 更に、OIDC ver1の仕様に乗っ取るのであれば、「{issクレームの値}/.well-known/openid-configuration」という URL でリクエストを受け付ける必要があることを念頭に置いて iss クレームの値を決める必要があります。
    • sub クレーム
      • sub クレームはユーザーの一意識別子を表す
      • IDトークンは「ユーザー認証」された後発行するものなので、そのユーザーを表す値がsub クレームです
    • aud クレーム
      • aud クレームはこのJWTの受け取り手が誰であるべきかを表すもの
      • IDトークンの場合、発行を依頼したクライアントアプリのクライアントIDです。
    • exp クレーム
      • exp クレームはJWTの有効期限を表す
      • IDトークンの場合、Unixエポック (1970年1月1日(世界標準時)) からの経過秒数となっています。ミリ秒ではないよ、秒だよ。
    • iat クレーム
      • iat クレームはJWTが発行された日時を表す
      • IDトークンの場合、exp クレームと同様にUnixエポックからの経過秒数です
    • auth_time クレーム
      • auth_time クレームはユーザーが認証された時刻を表す
      • Unixエポックからの経過秒数で表す
      • 「さっきの別のリクエストで認証したから、認証処理はパスしてIDトークン発行するヨン」ってときは auth_timeiat が異なる
      • この値は任意だが、認証後許容経過時間などでバリデーションがかかっている場合、必須となる
    • nonce クレーム
      • 主にリプレイアタックを防ぐ目的で、IDトークン発行依頼に nonce というリクエストパラメータがついてくることがあります。このときに、IDトークン発行側は、受け取ったnonce の値をそのままIDトークンに埋め込みます。
    • acr クレーム
      • acr クレームはユーザー認証が満たした認証コンテキスト ( パスワードによるシルバーレベルの認証、とか) のクラスを表す
      • 例) 値が "urn:mace:incommon:iap:silver" の場合、ユーザーはシルバーレベルの認証を受けたことを示します。
    • amr クレーム
      • amr クレームは認証手法を表している
      • パスワードベースの認証、ワンタイムパスワード、バイオメトリック認証など、さまざまな認証方法を示すことができます。
      • 値が ["pwd", "mfa"] の場合、ユーザーはパスワード認証とマルチファクタ認証を併用して認証されたことを示します。
    • azp クレーム
      • azp クレームは認可された対象者を表しています。
      • つまり認可されたクライアントアプリケーションのクライアント ID の値です。
      • このクレームは、ID トークンの発行を依頼したクライアントアプリケーションと認可されたクライアントアプリケーションが異なる場合、必要となります。

OAuthのクライアント認証とは

qiita.com

  • クライアント認証は、クライアントIDとクライアントシークレットを用いて認証を行う
    • クライアントID:アプリケーションの一意の識別子で、通常は公開情報
    • クライアントシークレット:クライアント自身を証明する秘密鍵

※ あんまり概要をつかめなかったのでまた今度

OAuthの全フロー

qiita.com

  • 4つの認可フローがある
  • 認可コードフロー
    • 基本的な認可フロー
      1. 認可サーバー内の認可エンドポイントに認可リクエストを投げて「認証情報が入力できる認可画面」を表示してもらう
      2. 認可サーバー内の認可決定エンドポイントで認証情報をもとに認可コードを発行する
      3. 認可サーバー内のトークンエンドポイントで認可コードをもとにアクセストークンを発行する
      4. アクセストークンさえ持てれば、APIを通じて許可された情報を取得できる
  • インプリシットフロー
    • 認可コードフローよりもフローが短い。「認可コード→アクセストークン」のフローが無い。直接アクセストークンを発行する
      1. 認可サーバー内の認可エンドポイントに認可リクエストを投げて「認証情報が入力できる認可画面」を表示してもらう
      2. 認可サーバー内の認可決定エンドポイントで認証情報をもとにアクセストークンを発行する
      3. アクセストークンさえ持てれば、APIを通じて許可された情報を取得できる
  • リソースオーナー・パスワード・クレデンシャルズフロー
    • インプリシットフローよりも短い。「認可リクエスト→認可画面表示」のフローがない。認可画面をサービス側に委託してしまうもの。
      1. サービス側で「認証・認可情報」を入力・選択する。
      2. 認可サーバー内のトークンエンドポイントで認証情報をもとにアクセストークンを発行する
      3. アクセストークンさえ持てれば、APIを通じて許可された情報を取得できる
  • クライアント・クレデンシャルズフロー
    • リソースオーナー・パスワード・クレデンシャルズフローよりも短い。「認可・認証情報の入力・選択」がない。"ユーザー自身の証明"がいらず、"サービスの証明"を認可サーバーに送る。
      1. 認可サーバー内のトークンエンドポイントで「クライアントIDとクライアントシークレット」をもとにアクセストークンを発行する
      2. アクセストークンさえ持てれば、APIを通じて許可された情報を取得できる
  • リフレッシュトークンフロー
    • 認可コードフローかリソースオーナー・パスワード・クレデンシャルズフローは、アクセストークンとともに 「 リフレッシュトーク 」というものも発行できる。もしそれを持っていた場合、このフローではリフレッシュトークンをトークンエンドポイントに渡すだけでアクセストークンを取得できる。
      1. 認可サーバー内のトークンエンドポイントで「リフレッシュトークン」をもとにアクセストークンを発行する
      2. アクセストークンさえ持てれば、APIを通じて許可された情報を取得できる

OpenID Connectの全フロー

qiita.com

  • OIDCはOAuth2を拡張したもの
    • OAuth2は アクセストーク 発行手順に関する仕様
    • OIDCは IDトーク の発行手順に関する仕様
  • それぞれの発行する場所は 認可エンドポイント というところ。そこでresponse_typeを要求する
    • code
    • token
    • id_token
  • 以下はresponse_typeのによる場合わけした説明である
  • response_type=code
    • scopeにopenidが含まれていないと認可コードフロー
    • scopeにopenidが含まれているとアクセストークンに加えてIDトークンも発行される
      1. 認可サーバー内の認可エンドポイントに認可リクエストを投げて「認証情報が入力できる認可画面」を表示してもらう
      2. 認可サーバー内の認可決定エンドポイントで認証情報をもとに認可コードを発行する
      3. 認可サーバー内のトークンエンドポイントで認可コードをもとにアクセストークンとIDトーク を発行する
  • response_type=token
    • インプリシットフローそのもの!scopeにopenidが含まれていようといまいと変わらない。
    • つまりこれ、OIDCだと使わない。
  • response_type=id_token
    • インプリシットフローのIDトークンだけ発行バージョン。アクセストークンは発行しない。
    • 一応以下フロー
      1. 認可サーバー内の認可エンドポイントに認可リクエストを投げて「認証情報が入力できる認可画面」を表示してもらう
      2. 認可サーバー内の認可決定エンドポイントで認証情報をもとにIDトーク を発行する
  • response_type=id_token token
    • インプリシットフローのアクセストークンとIDトークンの両方発行バージョン。
    • 一応以下フロー
      1. 認可サーバー内の認可エンドポイントに認可リクエストを投げて「認証情報が入力できる認可画面」を表示してもらう
      2. 認可サーバー内の認可決定エンドポイントで認証情報をもとにアクセストークンとIDトークンを発行する
  • response_type=code id_token
    • 「OIDCのresponse_type=id_token のフロー」と「OAuthの認可コードフロー」を同時に行うもの
      1. 認可サーバー内の認可エンドポイントに認可リクエストを投げて「認証情報が入力できる認可画面」を表示してもらう
      2. 認可サーバー内の認可決定エンドポイントで認証情報をもとに認可コードとIDトーク を発行する
      3. 認可サーバー内のトークンエンドポイントで認可コードをもとにアクセストークンを発行する
  • response_type=code token (scopeにopenidがあり)
    • 「OIDCのresponse_type=code (scopeにopenidあり)のフロー」のアクセストークンだけ認可決定エンドポイントからもらうタイプ
      1. 認可サーバー内の認可エンドポイントに認可リクエストを投げて「認証情報が入力できる認可画面」を表示してもらう
      2. 認可サーバー内の認可決定エンドポイントで認証情報をもとに認可コードとアクセストークンを発行する
      3. 認可サーバー内のトークンエンドポイントで認可コードをもとにIDトークンを発行する
  • response_type=code token (scopeにopenidがない)
    • 認可決定エンドポイントとトークンエンドポイント両方でアクセストークンを発行するタイプ。IDトークンは発行しない。
      1. 認可サーバー内の認可エンドポイントに認可リクエストを投げて「認証情報が入力できる認可画面」を表示してもらう
      2. 認可サーバー内の認可決定エンドポイントで認証情報をもとに認可コードとアクセストークンを発行する
      3. 認可サーバー内のトークンエンドポイントで認可コードをもとにアクセストークンを発行する
    • 2つのアクセストークンは同じじゃない場合がある。権限による。
  • response_type=code id_token token
    • 認可決定エンドポイントとトークンエンドポイント両方でアクセストークンとIDトークンを発行するタイプ。
      1. 認可サーバー内の認可エンドポイントに認可リクエストを投げて「認証情報が入力できる認可画面」を表示してもらう
      2. 認可サーバー内の認可決定エンドポイントで認証情報をもとに認可コードとアクセストークンとIDトークンを発行する
      3. 認可サーバー内のトークンエンドポイントで認可コードをもとにアクセストークンとIDトークンを発行する
  • response_type=none
    • 認証・認可しても認可決定エンドポイントから何も発行しない。(何がしたいん?)
      1. 認可サーバー内の認可エンドポイントに認可リクエストを投げて「認証情報が入力できる認可画面」を表示してもらう
      2. 入力してもらったけど、何も返さない。

あーーー疲れた。一旦ここまで。
また川崎さんにとてつもない感謝を。

認証系について調べてみた (概要編)【備忘録】

はじめに

認証系について曖昧な知識が多かったので、ここらで調査して整理する

調査

OAuthとOIDCとSAMLの違い

www.okta.com

  • OAuth
    • 認可
    • ユーザーがわざわざ認証情報(パスワードやメールアドレス)を共有しなくても、アプリケーションがユーザーに代わってアクションを起こせるもの
    • 例) hatenablogの投稿自動Tweet
  • OIDC
    • フェデレーション認証
    • アプリの認証(ログイン時)に、別のアプリケーションの認証を用いること。認証情報をは共有しなくていいところがみそ。
    • 例) YoutubeのログインにGoogleログインを使う
  • SAML
    • フェデレーション認証
    • SAML認証は特定のイントラネットやIdPにログインした後、特定のサービスに認証情報を再入力せずにアクセスできるようになるもの
    • SAMLはIdPとサービス・プロバイダーの間で認証・認可データを交換するためのXMLベースの標準のこと。
    • 例) Oktaにログインすれば他のサービスへのログインは自動でできる(SSO)

Identity Providers (IdPs)とは何なのか

www.okta.com

  • IdPとは
    • デジタルのIDを保存・管理するサービス
    • セキュリティを保護しながら、権限の追加や削除など、アクセス管理を行うための手段を提供する
  • IdPのワークフロー
    • Request: 別のログイン(Google, Facebookなど)から認証情報を入力することができる
    • Verification: IdPは(なんらかの)プロバイダーに確認し、ユーザーがどんな役割でアクセスができるかを判断する
    • Unlocking: ユーザーはサービスにアクセスできるようになり、訪問が記録される
  • IdPが解決してくれる組織の課題
    • いろんなサービスのパスワードを覚える必要がなくなる(セキュリティ的にも向上する)
    • 膨大な従業員の数だけのログインを準備する手間はもうなくなる
    • 誰かがミスをしたときに、足跡記録が残るので、どこが原因かがすぐに特定できる
    • データの連携がサービス間でとりやすい(らしい)→サイロ問題の解決

SSO ( Single Sign On ) とは

boxil.jp

  • SAML認証はSSOを実現する仕組みの一つ
  • IdPで共有しているユーザーの識別子はNameIDと呼ばれる
  • SSOとは?
    • 1つのIDとパスワードで認証を行うことで、複数のサービスにアクセスできる仕組みのこと
    • フェデレーション方式・エージェント方式・リバースプロキシ方式・代理認証方式などがある

SSOのそれぞれの方式の違いは?

Q. SSOにおける以下の方式をそれぞれ教えて。 - フェデレーション方式 - エージェント方式 - リバースプロキシ方式 - 代理認証方式 A. SSO (シングルサインオン) は、一度の認証で複数のサービスやアプリケーションにアクセスできるようにする技術です。以下に、主要なSSO方式を説明します。 フェデレーション方式: フェデレーション方式は、異なる組織間でのシングルサインオンを実現する方法です。主にSAML (Security Assertion Markup Language)、OAuth、OpenID Connectのようなプロトコルを使用します。これらのプロトコルを使用することで、異なる組織間で認証情報やアクセス許可をやり取りし、ユーザーが一度ログインするだけで複数のサービスを利用できるようになります。 エージェント方式: エージェント方式では、各アプリケーションにSSOエージェントと呼ばれるソフトウェアコンポーネントがインストールされます。ユーザーが認証を行うと、SSOエージェントがその情報をキャプチャし、セッション情報を生成します。ユーザーが他のアプリケーションにアクセスする際、エージェントはセッション情報を共有し、ユーザーが再度ログインすることなくアプリケーションを利用できるようにします。 リバースプロキシ方式: リバースプロキシ方式では、ネットワーク上のリバースプロキシサーバーがSSOを管理します。ユーザーは最初にリバースプロキシサーバーにアクセスし、認証を行います。認証後、リバースプロキシサーバーはユーザーのアクセス許可をチェックし、アクセス許可されたアプリケーションに対して、認証済みのセッション情報を引き継ぎます。これにより、ユーザーは一度のログインで複数のアプリケーションにアクセスできます。 代理認証方式: 代理認証方式では、一つの認証システムが他のシステムの認証情報を代理で保持します。ユーザーは、代理認証システムにログインし、そのシステムが他のアプリケーションの認証情報を代理で提供します。これにより、ユーザーは一度のログインで複数のアプリケーションにアクセスできます。代理認証方式は、ユーザー名とパスワードのペアを使用することが一般的ですが、他の認証方法(例えば、バイオメトリクスワンタイムパスワード)も組み合わせることができます。この方式の利点は、認証情報が一元管理されているため、ユーザーのパスワード管理が容易になることです。ただし、代理認証システムが標的となるリスクが高まるため、セキュリティ対策が重要となります。 これらのSSO方式は、それぞれ異なる認証シナリオやセキュリティ要件に応じて使用されます。適切なSSO方式を選択することで、ユーザーエクスペリエンスの向上やセキュリティ強化、管理コスト削減などの効果が期待できます。
引用元:ChatGPT

フェデレーション認証が一番いいっぽいな。