> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tiro.ooo/llms.txt
> Use this file to discover all available pages before exploring further.

# OAuthでユーザーの同意にもとづいて連携する

> メンバーがAPIキーを発行しなくても、一度の同意で社内システムがそのメンバーのTiroノートにアクセスできるOAuth 2.0連携の方法。

OAuthアプリを使うと、メンバーが個別にAPIキーを発行しなくても、社内ポータルがそのメンバーのノートを代わりに取得できます。メンバーはTiroへのログインと同意画面を一度通るだけで、アプリはそのユーザーが持つ権限の範囲内でのみ動作します。

## どのようなときにOAuthアプリを使いますか？

| 状況                                            | 推奨する方法                                                   |
| --------------------------------------------- | -------------------------------------------------------- |
| 社内ポータルやコラボレーションツールで、**ログイン中のメンバー本人**のノートを表示する | **OAuthアプリ**（このページ）                                      |
| バッチ処理やプロビジョニングのスクリプトが、**ユーザーを介さずに**組織のデータを扱う  | [組織APIキー](/ja/developers/organization/org-integration)   |
| 個人が自分のツールで自分のノートを扱う                           | [アカウントAPIキー](/ja/developers/fundamentals/authentication) |

OAuthアプリはAuthorization Code方式のみに対応しており、PKCEが必須です。ユーザーを介さずにトークンを取得する `client_credentials` 方式は提供していないため、サーバー間の連携にはAPIキーをご利用ください。

## ステップ1. OAuthアプリを登録する

組織の **Admin** または **Developer** が[Tiro Platform](https://platform.tiro.ooo)で登録します。OAuthアプリは組織の範囲でのみ登録できます。

<Steps>
  <Step title="OAuthアプリの画面を開く">
    Tiro Platformのサイドメニューから、組織の **\[OAuthアプリ]** メニューを開き、**\[OAuthアプリを登録]** ボタンを押します。
  </Step>

  <Step title="アプリ情報を入力する">
    アプリ名、**\[リダイレクトURI]**、ユーザーにリクエストする **\[スコープ]** を入力します。リダイレクトURIは `https://` で始まる必要があり、URLにユーザー情報や `#` 以降のフラグメントが含まれていると登録できません。認証後に戻るURLと1文字まで完全に一致する必要があります。登録後は変更できないため、URLが変わった場合はアプリを新しく登録してください。
  </Step>

  <Step title="Client IDとClient Secretを保存する">
    登録の直後に、画面へ **Client ID** と **Client Secret** が表示されます。Client Secretが表示されるのはこのときの1回限りで、あとから確認することはできません。サーバーの環境変数など、安全な場所にすぐ保存してください。
  </Step>
</Steps>

`http://localhost` はリダイレクトURIとして登録できません。ローカルで開発する場合は、HTTPSトンネルや開発用のドメインをリダイレクトURIとして登録し、そのURLからコールバックをローカルへ転送する構成にしてください。

<Note>
  OAuthアプリの登録画面は、順次公開しています。メニューが表示されない場合は、導入担当マネージャーまたは[partners@theplato.io](mailto:partners@theplato.io)へご依頼いただければ、アプリを発行いたします。
</Note>

<Warning>
  Client Secretは、登録画面を閉じると再表示できません。紛失した場合は、OAuthアプリ一覧で、そのアプリの行の右にある更新アイコンから新しい値を受け取ってください。再発行すると現在のシークレットは直ちに使えなくなるため、アプリを利用しているサーバーの設定を変更する準備をしてから進めてください。アプリを破棄すると、そのアプリで結ばれたユーザーの接続もあわせて解除されます。
</Warning>

### リクエストできるスコープ

OAuthアプリがリクエストできるのは、ユーザーが委任できるスコープのみです。組織管理用のスコープ（`organization_member:*`、`workspace:*`、`session:write`）は委任できず、組織APIキーでのみご利用いただけます。

| Scope                         | アプリができること                            |
| ----------------------------- | ------------------------------------ |
| `note:read`                   | ノート一覧、メタデータ、文字起こしの取得                 |
| `note:write`                  | ノートのタイトルなどメタデータの更新。`note:read` を含みます |
| `note_summary:read`           | ノートの要約（1ページの文書）の取得                   |
| `note_document:read`          | 生成された文書の取得                           |
| `note_document_template:read` | 文書テンプレートの一覧と内容の取得                    |
| `folder:read`                 | フォルダの取得                              |
| `folder:write`                | フォルダの作成、更新、削除。`folder:read` を含みます    |
| `wiki:read`                   | Wikiの取得と検索                           |
| `word_memory:read`            | 単語帳の取得                               |

アプリが実際に届く範囲は、ユーザーが同意したスコープと、そのユーザーがTiroで持つ権限が重なる部分です。ユーザーが閲覧できないワークスペースやノートは、スコープの同意を得ても取得できません。

## ステップ2. ユーザーの同意を得る

アプリからユーザーをTiroの認証ページへ遷移させます。まずランダムな `code_verifier` を生成し、その文字列をUTF-8バイトでSHA-256ハッシュしたあと、パディングなしのBase64URLでエンコードした値を `code_challenge` として送ります。16進数の文字列やハッシュの生バイトをそのまま送ると、トークン交換の段階で拒否されます。

```text theme={"system"}
GET https://api.tiro.ooo/v1/oauth/authorize
  ?response_type=code
  &client_id=YOUR_CLIENT_ID
  &redirect_uri=https://your-app.example.com/tiro/callback
  &scope=note:read%20note_summary:read
  &state=RANDOM_STATE
  &code_challenge=CODE_CHALLENGE
  &code_challenge_method=S256
  &resource=https://api.tiro.ooo
```

| パラメータ                                     | 説明                                                                     |
| ----------------------------------------- | ---------------------------------------------------------------------- |
| `response_type`                           | 常に `code`                                                              |
| `client_id`                               | 登録したアプリのClient ID                                                      |
| `redirect_uri`                            | 登録したリダイレクトURIと完全に同じ値                                                   |
| `scope`                                   | 半角スペース区切りのスコープ一覧。アプリに登録したスコープの部分集合のみリクエストでき、省略した場合は登録したスコープ全体をリクエストします |
| `state`                                   | 予測できないランダムな文字列。コールバックに同じ値が返ってきます                                       |
| `code_challenge`, `code_challenge_method` | PKCEの値。方式は `S256` のみ対応しています                                            |
| `resource`                                | トークンを使用する対象。External APIは `https://api.tiro.ooo`                       |

ユーザーがTiroアカウントでログインし、同意画面でアプリ名とリクエストされたスコープを確認して承認すると、リダイレクトURIへ `code` と `state` が返ります。

```text theme={"system"}
https://your-app.example.com/tiro/callback?code=AUTHORIZATION_CODE&state=RANDOM_STATE
```

### コールバックではstateを必ず検証してください

トークンを交換する前に、まず `state` を確認します。authorizeリクエストのときに生成した値をユーザーのセッションに保存しておき、コールバックで返ってきた値と一致するか比較したうえで、保存した値を削除します。値がない場合や一致しない場合は、その時点でリクエストを中断します。

この検証を省くと、攻撃者が開始した認証の結果が被害者のセッションに結び付き、被害者が知らないアカウントへ接続されるログインCSRFが可能になります。

<Note>
  SSOを利用している組織のメンバーは、同意画面の前に、普段どおり社内のIdPでログインします。同意画面はそのメンバーのTiroアカウントを基準に表示され、アプリが引き継ぐのもそのアカウントの権限だけです。
</Note>

## ステップ3. トークンを交換する

検証を終えた `code` を、サーバー側でアクセストークンに交換します。以下の例では、コールバックで受け取った認可コードとステップ2で作成した `code_verifier` を環境変数から読み込みます。Client Secretはサーバーでのみ扱い、ブラウザやアプリのコードには入れないでください。認可コードは一度しか使えないため、2回送ると `invalid_grant` で拒否されます。

<CodeGroup>
  ```bash cURL theme={"system"}
  curl -X POST https://api.tiro.ooo/v1/oauth/token \
    -u "$TIRO_CLIENT_ID:$TIRO_CLIENT_SECRET" \
    -d grant_type=authorization_code \
    -d code="$AUTHORIZATION_CODE" \
    -d redirect_uri="https://your-app.example.com/tiro/callback" \
    -d code_verifier="$CODE_VERIFIER" \
    -d resource="https://api.tiro.ooo"
  ```

  ```javascript Node.js theme={"system"}
  const authorizationCode = process.env.AUTHORIZATION_CODE;
  const codeVerifier = process.env.CODE_VERIFIER;
  const credentials = Buffer.from(
    process.env.TIRO_CLIENT_ID + ":" + process.env.TIRO_CLIENT_SECRET
  ).toString("base64");

  const response = await fetch("https://api.tiro.ooo/v1/oauth/token", {
    method: "POST",
    headers: {
      Authorization: "Basic " + credentials,
      "Content-Type": "application/x-www-form-urlencoded",
    },
    body: new URLSearchParams({
      grant_type: "authorization_code",
      code: authorizationCode,
      redirect_uri: "https://your-app.example.com/tiro/callback",
      code_verifier: codeVerifier,
      resource: "https://api.tiro.ooo",
    }),
  });

  const token = await response.json();
  ```

  ```python Python theme={"system"}
  import os
  import requests

  authorization_code = os.environ["AUTHORIZATION_CODE"]
  code_verifier = os.environ["CODE_VERIFIER"]

  response = requests.post(
      "https://api.tiro.ooo/v1/oauth/token",
      auth=(os.environ["TIRO_CLIENT_ID"], os.environ["TIRO_CLIENT_SECRET"]),
      data={
          "grant_type": "authorization_code",
          "code": authorization_code,
          "redirect_uri": "https://your-app.example.com/tiro/callback",
          "code_verifier": code_verifier,
          "resource": "https://api.tiro.ooo",
      },
  )

  token = response.json()
  ```

  ```go Go theme={"system"}
  authorizationCode := os.Getenv("AUTHORIZATION_CODE")
  codeVerifier := os.Getenv("CODE_VERIFIER")

  form := url.Values{}
  form.Set("grant_type", "authorization_code")
  form.Set("code", authorizationCode)
  form.Set("redirect_uri", "https://your-app.example.com/tiro/callback")
  form.Set("code_verifier", codeVerifier)
  form.Set("resource", "https://api.tiro.ooo")

  req, err := http.NewRequest(
      "POST",
      "https://api.tiro.ooo/v1/oauth/token",
      strings.NewReader(form.Encode()),
  )
  if err != nil {
      return err
  }
  req.SetBasicAuth(os.Getenv("TIRO_CLIENT_ID"), os.Getenv("TIRO_CLIENT_SECRET"))
  req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

  resp, err := http.DefaultClient.Do(req)
  ```

  ```kotlin Kotlin + Spring theme={"system"}
  val authorizationCode = System.getenv("AUTHORIZATION_CODE")
  val codeVerifier = System.getenv("CODE_VERIFIER")
  val clientId = System.getenv("TIRO_CLIENT_ID")
  val clientSecret = System.getenv("TIRO_CLIENT_SECRET")

  val form = LinkedMultiValueMap<String, String>()
  form.add("grant_type", "authorization_code")
  form.add("code", authorizationCode)
  form.add("redirect_uri", "https://your-app.example.com/tiro/callback")
  form.add("code_verifier", codeVerifier)
  form.add("resource", "https://api.tiro.ooo")

  val token = RestClient.create()
      .post()
      .uri("https://api.tiro.ooo/v1/oauth/token")
      .headers { headers -> headers.setBasicAuth(clientId, clientSecret) }
      .body(form)
      .retrieve()
      .body(TokenResponse::class.java)
  ```
</CodeGroup>

```json theme={"system"}
{
  "access_token": "eyJhbGciOi...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "note:read note_summary:read"
}
```

クライアント認証は、HTTP Basicとボディのパラメータ（`client_id`、`client_secret`）の両方に対応しています。

<Warning>
  `expires_in` は秒単位の残り時間で、アプリの設定によって変わります。値をコードに固定せず、レスポンスで返ってきた値をそのままお使いください。レスポンスに `refresh_token` があわせて返るアプリは以下の更新手順に従い、返らないアプリは、有効期限が切れる前にユーザーを再び認証ページへ遷移させてください。
</Warning>

## ステップ4. APIを呼び出す

アクセストークンは、APIキーと同じ方法で `Authorization` ヘッダーに入れます。呼び出せるエンドポイントとレスポンス形式は、[API概要](/ja/developers/fundamentals/api-overview)にまとめたものと同じです。

```bash theme={"system"}
curl https://api.tiro.ooo/v1/external/notes \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

レスポンスに含まれるノートは、同意したユーザーがアプリで閲覧できる範囲です。そのユーザーのアカウントAPIキーで呼び出したときと同じになります。

## ステップ5. トークンを更新する

トークン交換のレスポンスに `refresh_token` があれば、ユーザーから改めて同意を得ることなく、新しいアクセストークンを取得できます。

<CodeGroup>
  ```bash cURL theme={"system"}
  curl -X POST https://api.tiro.ooo/v1/oauth/token \
    -u "$TIRO_CLIENT_ID:$TIRO_CLIENT_SECRET" \
    -d grant_type=refresh_token \
    -d refresh_token="$REFRESH_TOKEN"
  ```

  ```javascript Node.js theme={"system"}
  const response = await fetch("https://api.tiro.ooo/v1/oauth/token", {
    method: "POST",
    headers: {
      Authorization: "Basic " + credentials,
      "Content-Type": "application/x-www-form-urlencoded",
    },
    body: new URLSearchParams({
      grant_type: "refresh_token",
      refresh_token: process.env.REFRESH_TOKEN,
    }),
  });

  const token = await response.json();
  ```

  ```python Python theme={"system"}
  response = requests.post(
      "https://api.tiro.ooo/v1/oauth/token",
      auth=(os.environ["TIRO_CLIENT_ID"], os.environ["TIRO_CLIENT_SECRET"]),
      data={
          "grant_type": "refresh_token",
          "refresh_token": os.environ["REFRESH_TOKEN"],
      },
  )

  token = response.json()
  ```
</CodeGroup>

* リフレッシュトークンは一度しか使えません。レスポンスに新しいリフレッシュトークンが返ってきたら、以前の値は破棄して新しい値を保存してください。
* 使用済みのリフレッシュトークンを再送すると、盗用と判断して、そのユーザー接続のトークンをすべて無効化します。この場合は、ユーザーから改めて同意を得る必要があります。
* 長期間更新されていない接続は期限切れになります。更新のたびに有効期限が延びるため、継続して使っている接続が切れることはありません。
* アプリを破棄した場合や、ユーザーが接続を解除した場合、更新は拒否されます。

<Tip>
  更新リクエストを送ったあと、ネットワークの問題でレスポンスを受け取れなかった場合は、同じリフレッシュトークンを再送しないでください。サーバー側ではすでに入れ替えが完了している可能性があります。保存したトークンを削除して、ユーザーに再認証をご案内するほうが安全です。
</Tip>

## 接続を解除する

ユーザーがアプリの利用をやめた場合は、リフレッシュトークンを破棄して接続を解除します。すでに発行済みのアクセストークンは、`expires_in` を過ぎれば自然に期限切れになります。

<CodeGroup>
  ```bash cURL theme={"system"}
  curl -X POST https://api.tiro.ooo/v1/oauth/revoke \
    -u "$TIRO_CLIENT_ID:$TIRO_CLIENT_SECRET" \
    -d token="$REFRESH_TOKEN"
  ```

  ```javascript Node.js theme={"system"}
  await fetch("https://api.tiro.ooo/v1/oauth/revoke", {
    method: "POST",
    headers: {
      Authorization: "Basic " + credentials,
      "Content-Type": "application/x-www-form-urlencoded",
    },
    body: new URLSearchParams({ token: process.env.REFRESH_TOKEN }),
  });
  ```

  ```python Python theme={"system"}
  requests.post(
      "https://api.tiro.ooo/v1/oauth/revoke",
      auth=(os.environ["TIRO_CLIENT_ID"], os.environ["TIRO_CLIENT_SECRET"]),
      data={"token": os.environ["REFRESH_TOKEN"]},
  )
  ```
</CodeGroup>

## サーバーメタデータ

エンドポイントと対応方式は、標準のメタデータ文書で確認できます。

```text theme={"system"}
GET https://api.tiro.ooo/.well-known/oauth-authorization-server
```

## よくあるエラー

| エラー                               | 原因と対処                                                                                                   |
| --------------------------------- | ------------------------------------------------------------------------------------------------------- |
| authorizeリクエストが400                | リダイレクトURIが登録した値と異なります。末尾のスラッシュやクエリ文字列の違いも不一致として扱います。                                                    |
| トークン交換が `invalid_grant`           | 認可コードをすでに使用済みか、期限が切れています。ユーザーを再び認証ページへ遷移させてください。`code_verifier` が `code_challenge` と一致しない場合も同じエラーになります。 |
| トークン交換が `invalid_client`          | Client IDまたはClient Secretが誤っています。アプリを破棄した場合も同様です。                                                       |
| authorizeが `invalid_scope`        | アプリに登録していないスコープをリクエストしています。登録したスコープの部分集合のみリクエストできます。                                                    |
| API呼び出しが `403 insufficient_scope` | 同意を得たスコープでは呼び出せないAPIです。必要なスコープは[認証](/ja/developers/fundamentals/authentication)で確認してください。               |

***

**関連ページ**: [認証](/ja/developers/fundamentals/authentication) · [組織単位の連携](/ja/developers/organization/org-integration) · [API概要](/ja/developers/fundamentals/api-overview)
