Zum Inhalt

OAuth2 und verknüpfte Rollen

Bostie hält einen glänzenden Schlüssel hoch

Mit OAuth2 gewährt ein Benutzer deiner App Zugriff auf sein Konto: sein Profil, seine Server, seine verknüpften Konten, das Recht, ihn einem Server hinzuzufügen, oder eine Rollenverbindung für verknüpfte Rollen. bostie baut die Autorisierungs-URLs und ruft die API mit dem Token auf, das der Benutzer gewährt.

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",
                )
            ],
        )

Autorisierungs-URLs

oauth2.authorize_url(client_id, scopes=[...], redirect_uri=..., state=...) gibt die URL zurück, die den Benutzer bittet, scopes zu gewähren, und mit einem code zurück zu redirect_uri weiterleitet. Scopes sind OAuth2Scope-Mitglieder, etwa OAuth2Scope.IDENTIFY für identify, eines für jeden Scope, den Discord dokumentiert; ein einfacher String funktioniert für einen Scope, den Discord hinzufügt, bevor bostie ihn benennt. Prüfe, dass die Weiterleitung den state mitbringt, den du gesendet hast, damit keine andere Seite den Benutzer mit ihrem Code anmelden kann. Es nimmt außerdem:

  • code_challenge, die S256-Challenge eines PKCE-Verifiers, den exchange_code dann als code_verifier sendet; das Snippet berechnet einen;
  • prompt="none", um den Zustimmungsbildschirm für einen Benutzer zu überspringen, der die Scopes schon gewährt hat, oder "consent", um ihn erneut anzuzeigen;
  • integration_type, um die App auf einem Server oder für den Benutzer zu installieren.

bostie baut nur den Authorization Code Grant. Discord rät vom Implicit Grant ab, der das Token in die Weiterleitungs-URL setzt und kein Refresh-Token liefert, und empfiehlt stattdessen den Code Grant mit PKCE.

oauth2.invite_url(client_id, permissions=..., scopes=[...]) gibt den Installationslink der App zurück, mit integration_type, guild_id, um einen Server vorauszuwählen, und disable_guild_select. Was es weglässt, kommt aus den Installationseinstellungen der App im Developer Portal.

Tokens

BearerClient() ohne Token erreicht nur rest.oauth2:

Methode Bewirkt
exchange_code(client_id=, client_secret=, code=, redirect_uri=, code_verifier=None) wandelt den Code aus der Weiterleitung in ein AccessToken um
refresh(client_id=, client_secret=, refresh_token=) ein neues Token, bevor das alte abläuft
revoke(client_id=, client_secret=, token=) beendet die Autorisierung eines Tokens und jedes Tokens, das es ausgestellt hat
exchange_client_credentials(client_id=, client_secret=, scopes=[...]) ein Token für den Eigentümer der App oder ihr Team

Ein AccessToken hat access_token, token_type, expires_in in Sekunden, scope (die gewährten Scopes, durch Leerzeichen getrennt) und refresh_token. Ein Code Grant, der den Scope bot enthielt, gibt außerdem den guild zurück, dem der Bot beigetreten ist, und einer mit webhook.incoming den webhook, den er erstellt hat. Die repr eines Tokens verbirgt die Geheimnisse, sodass das Loggen eines Tokens es nicht preisgibt.

Die API als der Benutzer aufrufen

async with BearerClient(access_token) as user: erreicht, was die Scopes des Tokens erlauben:

user.rest. Methoden Scope
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 zum Bearbeiten
oauth2 fetch_authorization, das sagt, was das Token wem gewährt beliebig
lobbies die Benutzerseite von Social-SDK-Lobbys sdk.social_layer

Mit dem Scope guilds.join kann der Bot den Benutzer einem Server hinzufügen, auf dem er selbst ist: await bot.rest.members.add(guild_id, user_id, access_token=token.access_token), das das neue Member zurückgibt, oder None, wenn der Benutzer schon Mitglied war.

Verknüpfte Rollen

Eine verknüpfte Rolle ist eine Serverrolle, die Benutzer bekommen, indem sie Bedingungen an eine Verbindung zu deiner App erfüllen, etwa „Level mindestens 10“. Die App deklariert einmal die Werte, die sie meldet, und meldet dann die Werte jedes Benutzers mit seinem Token:

  1. Registriere bis zu 5 Metadatensätze mit bot.rest.application.update_role_connection_metadata(application_id, [...]), jeder ein ApplicationRoleConnectionMetadata mit type, key, name und description. register_metadata im Snippet tut das mit einem RESTClient.
  2. Setz die Linked Roles Verification URL der App im Developer Portal auf eine Seite, die den OAuth2-Ablauf mit dem Scope role_connections.write startet.
  3. Ruf nach dem Austausch user.rest.users.update_role_connection(application_id, platform_name=..., platform_username=..., metadata={"level": "12"}) auf. Metadatenwerte sind Strings, so wie Discords API sie annimmt.

Das Beispiel für verknüpfte Rollen ist eine vollständige Verifizierungsseite. fmt.guild_navigation("linked-roles", role_id) verlinkt die verknüpften Rollen eines Servers im Chat.