OAuth2 und verknüpfte Rollen¶

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, denexchange_codedann alscode_verifiersendet; 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:
- Registriere bis zu 5 Metadatensätze mit
bot.rest.application.update_role_connection_metadata(application_id, [...]), jeder einApplicationRoleConnectionMetadatamittype,key,nameunddescription.register_metadataim Snippet tut das mit einemRESTClient. - Setz die Linked Roles Verification URL der App im Developer Portal auf eine Seite, die den OAuth2-Ablauf mit dem Scope
role_connections.writestartet. - 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.