MisskeyのOAuth2.0認可について・・・お話します・・・
今回はみすてむず いず みすきーしすてむず (4) Advent Calendar 2023の13日目への投稿となります。やったね。
MisskeyのOAuth2.0はIndieAuthを元に作られており、他の一般的な実装とはすこし異なります。
この記事ではそんなMisskeyのOAuth2.0について仕様を読みながら書いていきます。
普通のOAuth2.0と違うこと
client_id
が URL でなければならない- クライアントの情報は microformats で記述しなければならない
以上の2つが一般的なものとは大きく異なるようです。
認可サーバーのメタデータ取得
misskeyには /.well-known/oauth-authorization-server
というエンドポイントが実装されています。
これは、oauth2.0のエンドポイントや、スコープなどについての情報が得られるようです。
仕様はRFC8414で定義されているらしいです。
また、レスポンスは以下の形式で返されるようです。
{
/**
* 認可サーバーのURL
*/
issuer: string;
/**
* 認可エンドポイント
*/
authorization_endpoint: string;
/**
* トークンエンドポイント
*/
token_endpoint: string;
/**
* サポートされてるスコープの一覧
*/
scopes_supported: string[];
/**
* サポートされてる認可リクエストのレスポンスタイプの一覧
*
* 現時点では `code` のみ
*/
response_types: string[];
/**
* サポートされてるトークンのgrantタイプの一覧
*
* 現時点では `authorization_code` のみ
*/
grant_types_supported: string[];
/**
* ドキュメントへのリンク
*/
service_documentation?: string;
/**
* PKCEのサポートされてる形式の一覧
*
* 現時点では `S256` のみ
*/
code_challenge_methods_supported?: string[];
}
クライアント
登録の必要はなく、代わりにクライアントのwebページを作成する必要があります。
そのwebページはクライアントのIDとしても使用されます。
クライアントの情報はmicroformatsの仕様に則って記述する必要があり、最小限の例は以下のようになります。
<link rel="redirect_uri" href="./redirect" />
<div class="h-app">
<a href="./" class="u-url p-name">
クライアント名
</a>
</div>
で、これどうやって登録するの??
その必要はありません!!!
しれっとクライアントのIDとしてこのページヘのURLを使いましょう。
PKCEについて・・・お話します・・・
「PKCE」っていうのはね・・・
たとえば、認可コードを横取りされると・・・
気持ちが、良くないとか・・・
そういったことの対策として・・・「PKCE」というものがあるんだ・・・
いつ役立つのか
- URI経由で認可レスポンスを受け取りたい場合(
my-app://
みたいな) - クライアントがコンフィデンシャルでない場合
などに役立ちます。
また、スマホに限った話ではなく、WindowsやLinuxでもURI経由でソフトを呼び出せるためネイティブアプリなどで気をつける必要があるようです。
横取りの方法
認可レスポンスがサーバーから返される時に、不正なアプリケーションがredirect_uri
で使用されているスキームに登録されていた場合に起こります。
当然、不正なアプリケーションに認可コードが含まれる認可レスポンスが渡ってしまうので横取りサれてしまうのです。
PKCE3分クッキング
misskeyでは
S256
のみ対応してるのでS256
の場合で説明します。
~ PKCE3分クッキング ~
👩👵「「みなさんこんにちわ」」
👩「今回は、PKCEのcode_verifier
とcode_challenge
を生成していきます。」
👩「まず、code_verifier
用のランダムな文字列を生成します。」
👵「これは毎度生成したほうが良いですね。」
👩「これをsha-256
へハッシュドポテトにしてbase64uri
の形式にエンコードしていきます。」
👵「だいたい、500Wで30秒くらいが目安ですね。」
\ チーン /
👩「この出来上がったものがcode_challenge
で使うものになります。」
👩「これらは認可サーバーとクライアントのみが知る秘密の文字列なので、悪意あるアプリケーションは認可コードを横取りをできたとしてもcode_verifier
を検証する際に弾かれると言うことですね。」
👵「それではごきげんよう。」
PKCEで認可リクエスト
認可リクエスト時にはcode_challenge
と、そのcode_challenge
がどのようにして生成されたのか認可サーバーに知らせるためのcode_challenge_method
をパラメーターと一緒に渡します。
今回の例ではS256
の場合として説明しているのでcode_challenge_method
の内容はS256
としてください。
次に、認可コードをトークンに替えてもらう時の工程を説明します。
PKCEでトークンリクエスト
code_challenge
を検証するためcode_verifier
をトークンリクエストに含んで、認可サーバーへ送信します。
ここでようやく、PKCEの効果が発揮されます。
悪意あるアプリケーションはcode_verifier
がなんなのかわからず弾かれるという感じになります。
認可リクエスト
GET <authorization_endpoint>?<params>
認可リクエストエンドポイントは以下のクエリパラメーターを受け付けてるようです。
また、リクエストメソッドはGET
を受け付けています。
アクセスするべき場所はメタデータのauthorization_endpoint
に書いてあるようです。
名前 | 説明 | 任意 |
---|---|---|
response_type |
code のみ |
いいえ |
client_id |
クライアントのURL | いいえ |
redirect_uri |
リダイレクトURI | いいえ |
state |
CSRF攻撃を防ぐためのパラメーター | はい |
code_challenge |
PKCEコードチャレンジ | いいえ |
code_challenge_method |
PKCEコードチャレンジの形式 | いいえ |
scope |
要求するスコープ | いいえ |
PKCEについては任意ではなく必ず使用しないといけないっぽいです。
認可レスポンスについては通常のOAuth2.0と違いはなさそうです。
成功レスポンス
レスポンスは認可リクエスト後、ユーザーがredirect_uri
へリダイレクトされる形で返されます。
内容はクエリパラメータを介して渡されます。
名前 | 説明 |
---|---|
code |
生成された認可コード |
state |
リクエスト時に渡された state と同一 |
iss |
認可コードの発行者 |
認可コードは、トークンリクエストでトークンへ替える必要があります。
失敗レスポンス
名前 | 説明 |
---|---|
error |
エラーコード |
state |
リクエスト時に渡された state と同一 |
error
に渡されるエラーコードについて(一部)
invalid_request
リクエストに必須パラメーターなどが含まれてなかったりする場合などに返されます。
HTTPの
400 Bad Request
のようなものです。unsupported_response_type
認可サーバーがサポートしていない
response_type
を選択したときなどに返されます。invalid_scope
スコープが変な時に返されます。
これが返されたときには、スコープの名称に誤字脱字がないかチェックしてみましょう。
server_error
HTTPの
500 Internal Server Error
のようなもので、認可サーバー側でなんらかのエラーが起きた場合などに返されます。
他にもいろいろあるので、暇なときなどに仕様を読んでみましょう。
https://openid-foundation-japan.github.io/rfc6749.ja.html#rfc.section.4.1.2.1
トークンリクエスト
POST <token_endpoint>
Content-Type: application/x-www-form-urlencoded
アクセスするべき場所はメタデータのtoken_endpoint
に書いてあるようです。
トークンリクエストエンドポイントは以下のパラメーターを受け付けているようです。
また、リクエストメソッドはPOST
で受け付けており、パラメーターはapplication/x-www-form-urlencoded
として送信する必要があります。
名前 | 説明 |
---|---|
grant_type |
authorization_code のみ |
code |
成功レスポンスで受け取った認可コード |
client_id |
クライアントのID、ここで用意したページのURLを渡してあげましょう |
redirect_uri |
認可リクエストで使用したredirect_uri と同一である必要がある |
code_verifier |
認可リクエストで使用したcode_challenge と対になるcode_verifier |
成功レスポンス
以下の型に当てはまるJSONが返ってくるようです。
{
// アクセストークン
access_token: string;
// スコープ
scope: string;
// 有効期限
expires_in: number;
// リフレッシュトークン
refresh_token?: string;
}
失敗レスポンス
認可リクエストのものと同様です。
実際にリクエストを送信してみる
* JSランタイムにはdenoを使用する
base64uriのエンコーダーを適当に作る
import { nanoid } from "https://esm.sh/nanoid/";
import { encodeBase64 } from "https://deno.land/std/encoding/base64.ts";
const base64uri = (data: ArrayBuffer) =>
encodeBase64(data)
.replaceAll("+", "-")
.replaceAll("/", "_")
.replaceAll("=", "");
client_idなどを設定
const clientId = "https://ikasoba.github.io/misskey-oauth2-client-example";
const redirectUri =
"https://ikasoba.github.io/misskey-oauth2-client-example/redirect";
code_challenge生成
const codeChallengeMethod = "S256";
const codeVerifier = nanoid();
const codeChallenge = base64uri(
await crypto.subtle.digest("sha-256", new TextEncoder().encode(codeVerifier))
);
好きなインスタンスを設定して、メタデータを取る
const host = new URL("https://misskey.systems");
const meta = await fetch(
new URL("/.well-known/oauth-authorization-server", host)
).then((res) => res.json());
認可リクエスト用のリンクを作る
const authRequest = new URL(meta.authorization_endpoint, host);
authRequest.searchParams.set("response_type", "code");
authRequest.searchParams.set("client_id", clientId);
authRequest.searchParams.set("redirect_uri", redirectUri);
authRequest.searchParams.set("code_challenge", codeChallenge);
authRequest.searchParams.set("code_challenge_method", codeChallengeMethod);
authRequest.searchParams.set("scope", "write:notes");
ブラウザでやってみる
console.log("ブラウザでリクエスト", authRequest.toString());
取得したcodeを標準入力から取る
const code = prompt("認可コードを入力")!;
トークンをリクエスト
const tokenResponse = await fetch(new URL(meta.token_endpoint, host), {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
grant_type: "authorization_code",
code: code,
client_id: clientId,
redirect_uri: redirectUri,
code_verifier: codeVerifier,
}).toString(),
}).then((res) => res.json());
const token = tokenResponse.access_token;
投稿してみる
await fetch(new URL("/api/notes/create", host), {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
text: "OAuth2.0 テスト",
}),
});
おわりに
こんな感じでmisskeyのoauth2.0認可について、indieoauthの仕様を調べながら紹介してみました。
以上、いかそばの記事でした。