OAuth2 とリンクされたロール¶

OAuth2 を使うと、ユーザーは自分のアカウントへのアクセスをアプリに許可できます。対象は、プロフィール、参加しているサーバー、連携しているアカウント、そのユーザーをサーバーに追加する権利、リンクされたロールのためのロール接続です。bostie は認可 URL を作成し、ユーザーが許可したトークンで API を呼び出します。
import base64
import hashlib
import os
import secrets
import bostie
from bostie import (
AccessToken,
ApplicationRoleConnectionMetadata,
ApplicationRoleConnectionMetadataType,
OAuth2Scope,
oauth2,
)
CLIENT_ID = int(os.environ.get("CLIENT_ID", "0"))
CLIENT_SECRET = os.environ.get("CLIENT_SECRET", "")
REDIRECT_URI = "https://example.com/callback"
def login_url() -> tuple[str, str, str]:
state = secrets.token_urlsafe(16)
verifier = secrets.token_urlsafe(48)
digest = hashlib.sha256(verifier.encode()).digest()
challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode()
url = oauth2.authorize_url(
CLIENT_ID,
scopes=[
OAuth2Scope.IDENTIFY,
OAuth2Scope.GUILDS,
OAuth2Scope.ROLE_CONNECTIONS_WRITE,
],
redirect_uri=REDIRECT_URI,
state=state,
code_challenge=challenge,
)
return url, state, verifier
async def finish_login(code: str, verifier: str) -> AccessToken:
async with bostie.BearerClient() as anonymous:
return await anonymous.rest.oauth2.exchange_code(
client_id=CLIENT_ID,
client_secret=CLIENT_SECRET,
code=code,
redirect_uri=REDIRECT_URI,
code_verifier=verifier,
)
async def describe_user(token: AccessToken) -> str:
async with bostie.BearerClient(token.access_token) as user:
me = await user.rest.users.fetch_current()
guilds = [guild.name async for guild in user.rest.guilds.list()]
return f"{me.username} is in {len(guilds)} guilds"
async def refresh(token: AccessToken) -> AccessToken:
if token.refresh_token is None:
return token
async with bostie.BearerClient() as anonymous:
return await anonymous.rest.oauth2.refresh(
client_id=CLIENT_ID,
client_secret=CLIENT_SECRET,
refresh_token=token.refresh_token,
)
async def register_metadata() -> None:
async with bostie.RESTClient() as client:
await client.rest.application.update_role_connection_metadata(
CLIENT_ID,
[
ApplicationRoleConnectionMetadata(
type=ApplicationRoleConnectionMetadataType.INTEGER_GREATER_THAN_OR_EQUAL,
key="level",
name="Level",
description="The player's level in the game",
)
],
)
認可 URL¶
oauth2.authorize_url(client_id, scopes=[...], redirect_uri=..., state=...) は、ユーザーに scopes の許可を求め、code を付けて redirect_uri にリダイレクトして戻る URL を返します。スコープは OAuth2Scope のメンバーで、identify に対応する OAuth2Scope.IDENTIFY のように、Discord が文書化しているスコープごとに 1 つずつあります。bostie が名前を付ける前に Discord が追加したスコープには、ただの文字列も使えます。リダイレクトに送ったときと同じ state が含まれていることを確認してください。そうすれば、別のサイトが自分のコードでユーザーをログインさせることはできません。ほかにも次の引数を受け取ります。
code_challenge。PKCE の検証子から求めた S256 のチャレンジで、exchange_codeが後でその検証子をcode_verifierとして送ります。スニペットでは検証子を 1 つ計算しています。prompt="none"。すでにスコープを許可したユーザーには同意画面を飛ばします。"consent"を指定すると再び表示します。integration_type。アプリをサーバーにインストールするか、ユーザーにインストールするかを指定します。
bostie が作成するのは、認可コードグラントの URL だけです。インプリシットグラントはトークンをリダイレクト URL に含め、リフレッシュトークンも発行しないため、Discord はこれを推奨しておらず、代わりに PKCE 付きのコードグラントを勧めています。
oauth2.invite_url(client_id, permissions=..., scopes=[...]) はアプリのインストールリンクを返します。integration_type、あらかじめサーバーを選択しておく guild_id、disable_guild_select も受け取ります。省いた項目は、Developer Portal にあるアプリのインストール設定から取られます。
トークン¶
トークンなしの BearerClient() から使えるのは rest.oauth2 だけです。
| メソッド | 動作 |
|---|---|
exchange_code(client_id=, client_secret=, code=, redirect_uri=, code_verifier=None) |
リダイレクトで受け取ったコードを AccessToken に交換します |
refresh(client_id=, client_secret=, refresh_token=) |
古いトークンが失効する前に新しいトークンを取得します |
revoke(client_id=, client_secret=, token=) |
トークンと、そのトークンから発行されたすべてのトークンの認可を終了します |
exchange_client_credentials(client_id=, client_secret=, scopes=[...]) |
アプリのオーナー(またはそのチーム)のトークンを取得します |
AccessToken は access_token、token_type、秒単位の expires_in、scope(許可されたスコープを空白で区切ったもの)、refresh_token を持ちます。bot スコープを含むコードグラントでは、Bot が参加した guild も返されます。webhook.incoming を含む場合は、作成された webhook も返されます。トークンの repr は秘密情報を隠すので、ログに出しても漏れることはありません。
ユーザーとして API を呼び出す¶
async with BearerClient(access_token) as user: からは、トークンのスコープが許す範囲にアクセスできます。
user.rest. |
メソッド | スコープ |
|---|---|---|
users |
fetch_current |
identify |
users |
list_connections |
connections |
users |
fetch_role_connection、update_role_connection、delete_role_connection |
role_connections.write |
guilds |
list |
guilds |
members |
fetch_current(guild_id) |
guilds.members.read |
commands |
fetch_permissions、list_permissions、edit_permissions |
編集には applications.commands.permissions.update |
oauth2 |
fetch_authorization。トークンが何を誰に許可しているかを返します |
任意 |
lobbies |
Social SDK のロビーのユーザー側の操作 | sdk.social_layer |
guilds.join スコープがあれば、Bot は自分が参加しているサーバーにそのユーザーを追加できます(await bot.rest.members.add(guild_id, user_id, access_token=token.access_token))。これは新しい Member を返し、ユーザーがすでにメンバーだった場合は None を返します。
リンクされたロール¶
リンクされたロールは、アプリとの接続について「レベル 10 以上」のような条件を満たしたユーザーに付与されるサーバーのロールです。アプリは報告する値を一度宣言し、その後、各ユーザーの値をそのユーザーのトークンで報告します。
bot.rest.application.update_role_connection_metadata(application_id, [...])で、最大 5 件のメタデータを登録します。それぞれtype、key、name、descriptionを持つApplicationRoleConnectionMetadataです。スニペットのregister_metadataは、RESTClientを使ってこれを行います。- Developer Portal で、アプリの Linked Roles Verification URL に、
role_connections.writeスコープで OAuth2 フローを開始するページを設定します。 - 交換の後、
user.rest.users.update_role_connection(application_id, platform_name=..., platform_username=..., metadata={"level": "12"})を呼び出します。Discord の API が受け取るとおり、メタデータの値は文字列です。
リンクされたロールのサンプルは、完全な認証ページです。fmt.guild_navigation("linked-roles", role_id) は、チャットでサーバーのリンクされたロールへのリンクを作ります。