是否可以使用discord oauth2生成一次性链接?

piah890a  于 2023-03-22  发布在  其他
关注(0)|答案(2)|浏览(190)

简介:

我所追求的是当用户注册我的discord.py机器人时:

  • 他们会收到一封一次性使用oauth2链接的电子邮件,该链接将要求他们登录discord
  • 登录后,我希望它能自动添加他们/邀请他们加入公会,如果他们还没有添加
  • 自动加入公会后,自动为他们指定一个注册角色

要点:

  • 我有一个HTTPPOST webhook,当用户从我用来处理用户注册的SAAS注册时,它会被触发
  • 这个机器人目前只在我的Discord服务器上,所以扩展这个机器人到多个服务器上并不是一个问题

我的尝试:

这是一些使用FastAPI的粗略代码草案。此代码未经测试,无法工作。

from fastapi import FastAPI
from fastapi.responses import RedirectResponse
from cryptography.fernet import Fernet
import base64
import time

app = FastAPI()

key = Fernet.generate_key()
fernet = Fernet(key)

# this would get fired from my HTTP POST webhook once the user sucessfully signs up 
@app.post("/generate_link/{user_id}")
def generate_link(user_id: int):
    encrypted_id = fernet.encrypt(str(user_id).encode())
    timestamp = str(int(time.time()))
    data = f"{encrypted_id.decode()}:{timestamp}"
    token = base64.urlsafe_b64encode(data.encode()).decode()

    # send one time use encrypted url to user via email API 
    email_api.send("mydomain/validate_link?token={token}")

# this would get fired when user goes to onetime generated link 
@app.get("/validate_link")
def validate_link(token: str):
    try:
        # Decode the token from the query parameter and split the encrypted ID and timestamp
        data = base64.urlsafe_b64decode(token.encode()).decode()
        encrypted_id, timestamp = data.split(":")
        timestamp = int(timestamp)

        # Check that the timestamp is within the valid range (for example 10 minutes)
        if time.time() - timestamp > 600:
            return "Link expired"

        # Decrypt the user ID and do something with it
        user_id = int(fernet.decrypt(encrypted_id.encode()).decode())
        # redirect user to oauth2 link with discord now that we have verified them? 
        return f"User ID: {user_id}"
    except:
        return "Invalid link"

我的终极问题:

  • 有没有内置的功能,做什么,我想在这里与不和谐oauth2,我可以只发送给用户一个链接后,电子邮件注册?
  • 或者我需要像我在这里使用FastAPI尝试的那样进行自定义吗?例如,将步骤分开,让一个步骤生成一次性URL,第二个步骤使用oauth2/adding to guild/adding role登录

感谢任何花时间阅读我的文章的人!

wr98u20j

wr98u20j1#

这是可能的。多亏了Oath 2,你可以做很多事情,比如添加某人到公会,并在这样的公会中给予他们角色,所有这些都是一次性的。我用Flask来做,因为我最习惯这样做,但这并不重要。
首先,你需要一个discord Application。一旦你有了它,你去OAuth2,URL生成器,并选择至少identifyguild.join。复制该URL并将其粘贴到代码中的变量中。重定向URI应该是你的Flask应用程序的URL到/回调。
在代码的开头完成其余的变量(如果你不知道如何获取角色和服务器的ID,请查看Discord开发模式)。
确保机器人在您的服务器上,并且至少具有CREATE_INSTANT_INVITEMANAGE_ROLES权限。
这就是一切,下面的代码应该工作.它所做的是使用OAth 2获取用户ID,然后使用它使其加入公会指定的角色.你现在可以通过电子邮件发送AUTORISATION_URL,或者如果你喜欢自己的URL的URL到你的Flask应用程序.
我知道Flask可能不适合“生产部署”,所以考虑在代码中添加waitress

from flask import Flask, request, redirect
import requests

API_ENDPOINT = 'https://discord.com/api/v10'
TOKEN_URL = "https://discord.com/api/oauth2/token"

OAUTH2_CLIENT_ID = "" #Your client ID
OAUTH2_CLIENT_SECRET = "" #Your client secret
OAUTH2_REDIRECT_URI = "http://localhost:5000/callback" #Your redirect URL
BOT_TOKEN = "" #"Your application token here"
REDIRECT_URL = "https://discord.com/" #Where you wish to redirect your user.
GUILD_ID = 0 #The ID of the guild you want them to join
ROLE_IDS = [0] #List of the IDs of the roles you want them to get
AUTORISATION_URL = "" #The obtained URL

app = Flask(__name__)

@app.route('/')
def main():
    return redirect(AUTORISATION_URL)


@app.route('/callback')
def callback():
    print("flag")
    if request.values.get('error'):
        return request.values['error']

    args = request.args
    code = args.get('code')

    data = {
        'client_id': OAUTH2_CLIENT_ID,
        'client_secret': OAUTH2_CLIENT_SECRET,
        'grant_type': 'authorization_code',
        'code': code,
        'redirect_uri': OAUTH2_REDIRECT_URI
    }
    headers = {
        'Content-Type': 'application/x-www-form-urlencoded'
    }

    r = requests.post("https://discord.com/api/v10/oauth2/token", data=data, headers=headers)
    r.raise_for_status()

    #Get the acces token
    access_token = r.json()["access_token"]

    #Get info of the user, to get the id
    url = f"{API_ENDPOINT}/users/@me"

    headers = {
        "Authorization": f"Bearer {access_token}",
        'Content-Type': 'application/json'
    }

    #This will contain the information
    response = requests.get(url=url, headers=headers)

    print(response.json())

    #Extract the id
    user_id = response.json()["id"]

    #URL for adding a user to a guild
    url = f"{API_ENDPOINT}/guilds/{GUILD_ID}/members/{user_id}"

    headers = {
        "Authorization": f"Bot {BOT_TOKEN}"
    }

    #These lines specifies the data given. Acces_token is mandatory, roles is an array of role ids the user will start with.
    data = {
        "access_token": access_token,
        "roles": ROLE_IDS
    }

    #Put the request
    response = requests.put(url=url, headers=headers, json=data)

    print(response.text)
    return redirect(REDIRECT_URL)

app.run(port=5000)
d8tt03nd

d8tt03nd2#

如果有人在Google上找到这个:
感谢Clement的回答,我能够得到我的最终解决方案。在做了更多的挖掘之后,我最终确定我可以通过添加state参数来使oauth2链接成为一次性使用链接。state参数可以与oauth2链接一起传递。在用户使用discord登录授权后,oauth2将它们重定向回服务器(在我的例子中是Flask)。然后在他们重定向之后,我们可以检查在url中传递的这个状态令牌是否已经使用或者还没有使用数据库(或者一些简单的东西,比如在磁盘数据库上的小用例)。我想给用户发送一个一次性的链接,这样他们就不能重用这个链接或与他们的朋友分享。

生成一个时间状态令牌:

我使用了一个hash来完成这个操作。这段代码是虚拟的模板代码,因为我不想透露我是如何做hash的,但是你可以用你想用什么来填充细节。然后我把它存储在一个数据库中,以便在用户点击aouth2回调端点时引用。

import hashlib 
import base64 

uniquestr = user_info_data_here + SALT
hash_obj = hashlib.md5(uniquestr.encode())
hash_digest = hash_obj.digest()
token = base64.urlsafe_b64encode(hash_digest).decode()

db.set(token, [user_info_id])

发送onetime oauth2链接给用户:

我使用smptlib在HTTP POST webhook被触发时给他们发了一封电子邮件。我使用了一个电子邮件html模板来使电子邮件看起来很漂亮。这也是虚拟代码,但如果你想使用我的代码,你可以在这里放你自己的信息

import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

   with smtplib.SMTP_SSL("mail.domain.com", 465) as server:
        msg = MIMEMultipart()
        msg['Subject'] = 'Welcome'
        msg['From'] = "support@domain.com"
        msg['To'] = user_email
        msg.preamble = 'Welcome'
        server.ehlo()
        server.login("support@domain.com", EMAIL_PASS)
        msg.attach(MIMEText(html_text, 'html', 'utf-8'))
        server.sendmail("support@domain.com", user_email, msg.as_string())

Oauth回调端点:

在用户使用链接并使用discord进行授权后,discord将重定向到您的oauth2回调端点。在此端点中,我们可以将用户添加到所需的公会,并为他们提供所需的角色,然后将他们重定向到一个漂亮的“You're all done”成功页面。一个关键注意事项是将他们添加到公会(如果他们还没有进入公会服务器),你需要发送一个put请求。然后要给予他们角色,你需要发送一个patch请求。

Config.py:

GUILD_ID = 123456
BOTH_ROLES = [123445, 1234456]
BOT_TOKEN = "yourtokenhere"
API_ENDPOINT = "https://discord.com/api/v10"
TOKEN_URL = "https://discord.com/api/oauth2/token"
CLIENT_ID = "123456"
CLIENT_SECRET = "yoursecrethere"
REDIRECT_URL = "yourdomain.com/oauthcallback"
SUCCESS_URL = "yourdomain.com/thank-you/"
OAUTH_URL = "yourgenerateddiscordoauthurl" + "&state="

回调端点:

@app.route('/oauthcallback')
def callback():
    if "code" not in request.args or "state" not in request.args: 
        return "Error", 404 
    if request.values.get('error'):
        return request.values['error']
    
    code = request.args["code"]
    state = request.args["state"]
    
    # make sure token exists and hasn't already been used
    if state not in db():
        return "Error", 404 
    
    # Get oauth access token
    data = {
        'client_id': config.CLIENT_ID,
        'client_secret': config.CLIENT_SECRET,
        'grant_type': 'authorization_code',
        'code': code,
        'redirect_uri': config.REDIRECT_URL
    }
    headers = {
        'Content-Type': 'application/x-www-form-urlencoded'
    }
    r = requests.post(config.TOKEN_URL, data=data, headers=headers)
    r.raise_for_status()
    access_token = r.json()["access_token"]

    # get user info with access token
    url = f"{config.API_ENDPOINT}/users/@me"
    headers = {
        "Authorization": f"Bearer {access_token}",
        'Content-Type': 'application/json'
    }
    response = requests.get(url=url, headers=headers)
    response.raise_for_status()
    user_id = response.json()["id"]

    # Add user to guild and add roles 
    url = f"{config.API_ENDPOINT}/guilds/{config.GUILD_ID}/members/{user_id}"
    headers = {
        "Authorization": f"Bot {config.BOT_TOKEN}"
    }
    data = {
        "access_token": access_token
    }
    response = requests.put(url=url, headers=headers, json=data)
    response.raise_for_status()
    data = {
        "access_token": access_token,
        "roles": config.BOTH_ROLES 
    }
    response = requests.patch(url=url, headers=headers, json=data)
    response.raise_for_status()
    db.delete(state)

    return redirect(config.SUCCESS_URL)

相关问题