是否可以使用Discord OAuth2生成一次性链接?

huangapple go评论112阅读模式
英文:

Is it possible to generate a one time link with discord oauth2?

问题

Intro:

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

  • 他们会收到一封一次性使用的OAuth2链接的电子邮件,该链接将要求他们登录到Discord。
  • 登录后,如果他们尚未添加到服务器,我希望它自动将他们添加/邀请他们进入服务器。
  • 在自动加入服务器后,自动为他们分配一个特定的角色以表示注册。

Key points:

  • 我有一个HTTP POST Webhook,当用户从我正在使用的SAAS处理用户注册时会触发它。
  • 该机器人目前仅在我的Discord服务器中,因此不需要考虑将该机器人扩展到多个服务器。

What I've tried:

这是使用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)

# 这将从我的HTTP POST Webhook中触发,一旦用户成功注册
@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()

    # 通过电子邮件API向用户发送一次性使用的加密URL
    email_api.send("mydomain/validate_link?token={token}")

# 当用户访问一次性生成的链接时,将触发此功能
@app.get("/validate_link")
def validate_link(token: str):
    try:
        # 解码查询参数中的令牌并拆分加密的ID和时间戳
        data = base64.urlsafe_b64decode(token.encode()).decode()
        encrypted_id, timestamp = data.split(":")
        timestamp = int(timestamp)

        # 检查时间戳是否在有效范围内(例如10分钟)
        if time.time() - timestamp > 600:
            return "链接已过期"

        # 解密用户ID并执行相应操作
        user_id = int(fernet.decrypt(encrypted_id.encode()).decode())
        # 现在我们已经验证了他们,是否将用户重定向到Discord的OAuth2链接?
        return f"用户ID:{user_id}"
    except:
        return "无效链接"

My ultimate question:

  • Discord的OAuth2是否内置了我想要的功能,我只需在电子邮件注册后发送用户一个链接?
  • 还是我需要像我在FastAPI中尝试的那样,创建自定义解决方案?例如,分成两个步骤,一个步骤生成一次性URL,第二步进行OAuth2登录/添加到服务器/分配角色。

感谢花时间阅读我的帖子的任何人!

英文:

Intro:

What I'm after is when a user signs up for my discord.py bot:

  • they get emailed a one time use oauth2 link that will ask them to login into discord
  • after logging in, I want it to automatically add them/invite them to the guild if they are not already added
  • upon automatically joining the guild, automatically give them a specific role for signing up

Key points:

  • I have a HTTP POST webhook that gets fired when a user signs up from the SAAS I'm using to handle user signs up
  • The bot is currently only in my discord server, so scaling this bot for multiple servers is not a concern

What I've tried:

This is some rough draft code using FastAPI. This code is not tested and does not work.

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"

My ultimate question:

  • Is there built in functionality to do what I want here with discord oauth2 and I can just send the user a link upon email sign up?
  • Or do I need something custom like my attempt here with FastAPI? For example, splitting the steps apart and having one step generate the one time url and the second step signing in with oauth2/adding to guild/adding role

Thanks for anyone who takes the time to read through my post!

答案1

得分: 1

以下是您要翻译的内容:

"It is possible. Thanks to OAuth2 you can do many things, such as add someone to a guild and give them roles in such a guild, all in one go. I use Flask to do it since I'm most comfortable with that, but it doesn't really matter.

First, you need a discord Application. Once you have it you go to OAuth2, URL generator and select at least identify and guild.join. Copy that URL and paste it in the variable in the code. The redirect URI should be the URL to /callback of your Flask App.

Complete the rest of the variables at the beginning of your code (if you don't know how to get the IDs of the roles and server check out Discord Development mode).

Make sure the bot is on your server and that it has at least the CREATE_INSTANT_INVITE and MANAGE_ROLES permissions on it.

That is everything, the code below should work. What it does is use OAuth2 to get the user's ID, and then uses it to make it join the guild with the specified roles. You can now email the AUTORISATION_URL, or if you prefer your URL, the URL to your Flask App.

I am aware that Flask may not be suitable for "production deployment" so consider adding waitress to the code.

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 access 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 specify the data given. Access_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)
英文:

It is possible. Thanks to Oath2 you can do many things, such as add someone to a guild and give them roles in such guild, all in one go. I use Flask to do it since I'm most confortable with that, but it doesn't really matter.

First, you need a discord Application. Once you have it you go to OAuth2, URL generator and select at least identify and guild.join. Copy that URL and paste it in the variable in the code. The redirect uri should be the url to /callback of your Flask App.

Complete the rest of the variables at the beggining of your code (if you don't know how to get the IDs of the roles and server check out Discord Development mode).

Make sure the bot is on your server and that it has at least the CREATE_INSTANT_INVITE and MANAGE_ROLES permissions on it.

That is everything, the code bellow should work. What it does is use OAth2 to get the users id, and then uses it make it join the guild with the specified roles. You can now email the AUTORISATION_URL, or if you prefer your own url the URL to your Flask App.

I am aware that Flask may not be suitable for "production deployment" so consider adding waitress to the code.

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)

答案2

得分: 1

以下是您提供的内容的中文翻译:

如果有人从 Google 找到这个:

感谢 Clement 的回答,我能够找到我的最终解决方案。经过进一步的研究,我最终确定我可以通过添加 "state" 参数将 OAuth2 链接设置为一次性链接。 "state" 参数可以与 OAuth2 链接一起传递。用户授权 Discord 登录后,OAuth2 会将他们重定向回您的服务器(在我这里是 Flask)。然后,当他们被重定向后,我们可以检查 URL 中传递的这个状态令牌是否已经使用,或者是否尚未使用,使用数据库(或像小型用例中的简单磁盘数据库一样的东西)。我希望发送给用户一次性链接,以便他们无法重复使用链接或与朋友分享它。

生成一次性状态令牌:

我使用了哈希来做到这一点。这段代码是一个虚拟模板代码,因为我不想透露我如何做哈希,但你可以根据你想要哈希的内容填充细节。然后,我将其存储在数据库中,以供用户在访问 OAuth2 回调端点时引用。

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

发送一次性 OAuth2 链接给用户:

我使用 smtplib 发送电子邮件,当 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'] = '欢迎'
        msg['From'] = "support@domain.com"
        msg['To'] = user_email
        msg.preamble = '欢迎'
        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 回调端点。在这个端点内,我们可以将用户添加到所需的服务器并赋予他们所需的角色,然后重定向他们到一个漂亮的 "您已完成" 成功页面。一个关键的注意是,如果他们尚未在服务器中(如果他们还没有在服务器中),您需要发送一个 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 "错误", 404
    if request.values.get('error'):
        return request.values['error']

    code = request.args["code"]
    state = request.args["state"]

    # 确保令牌存在且尚未被使用
    if state not in db():
        return "错误", 404

    # 获取 OAuth 访问令牌
    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"]

    # 使用访问令牌获取用户信息
    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"]

    # 将用户添加到服务器并添加角色
    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)

希望这些翻译对您有所帮助!

英文:

If anyone finds this from google:

Thanks to Clement's answer I was able to reach my final solution. After doing some more digging, I ultimately determined that I could make an oauth2 link a onetime use link by adding the state parameter. The state parameter can be passed with the oauth2 link. After the user authorizes with discord login, oauth2 redirects them back to your server (in my case Flask). Then after they have redirected we can check if this state token passed within the url has been used or not yet using a database (or something simple like on disk db for small use cases). I wanted to send users a onetime link so they would not be able to reuse the link or share it with their friends.

Generating one time state token:

I did this using a hash. This code is dummy template code because I don't want to disclose exactly how I'm doing the hash but you can fill in the details with what you'd like to hash with. I then store this in a database to reference later when the user hits the aouth2 callback endpoint.

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

I used smptlib to send them an email when the HTTP POST webhook is fired. I used an email html template to make the email look nice. This is also dummy code but you can put your own info in here if you want to use my code

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 Callback endpoint:

After the user uses the link and authorizes with discord, discord will redirect to your oauth2 callback endpoint. Within this endpoint we can add the user to the desired guild and give them desired roles and then redirect them to a pretty "You're all done" success page. One key note is to add them to the guild (if they're not inside of the guild server yet), you'll need to send a put request. Then to give them roles, you'll need to send a patch request.

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="

Callback endpoint:

@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)

huangapple
  • 本文由 发表于 2023年3月12日 09:56:28
  • 转载请务必保留本文链接:https://go.coder-hub.com/75710670.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定