DEV Community

Cover image for Discord OAuth, hard to understand, easy to use
Vxrpenter
Vxrpenter

Posted on

Discord OAuth, hard to understand, easy to use

Table Of Contents

Introduction

I once needed to connect users Discord accounts with their Steam accounts. So I just asked them to enter their SteamId and then connected them using a simple database table. I thought that was it, but then someone brought the fact to me, that people could enter someone else's SteamId to impersonate them and take actions in their account. So I tried finding a way to fix it and that lead me to my biggest coding nightmare, working with Discord OAuth.

What is even OAuth

Cloudflare defines OAuth as follows: "OAuth is a technical standard for authorizing users. It is a protocol for passing authorization from one service to another without sharing the actual user credentials, such as a username and password" [Source]

We can compile this down to the following statement, "OAuth allows us to access certain information without credential sharing". For Discord this allows us to get account connections, guilds (Discord servers) or even E-Mails without the user having to provide their password. It should be self explanatory why this is useful but to boil it down, it allows us to interact with certain user information in a safe and private manner.

Using OAuth

Let's look into OAuth from the problem described in the introduction. We need to connect a Discord UserId with a SteamId, so we need two different OAuth scopes. These are identify, which allows us to use the user/@me endpoint, for querying user profile information and the connections scope, which gives us access to the /users/@me/connections endpoint, which returns connections information.

So what steps do we need now, that we know what information we need to query?

  1. Creating the OAuth link
  2. Creating a simple WebServer for redirects
  3. Making the Api requests

Creating the OAuth link

The OAuth link, is generated by Discord and given to the user, for them to authorize the data transfer. It is really simple to create one, although you have to take some things into account.

Firstly, navigate to the Discord Developer Portal, then click on your application and find the OAuth section. Here we need to add the redirect, that Discord will send the user to. In our case we will simply use a localhost:

A field for entering redirects with the domain http://localhost:8080/discord/redirect/oauth inside it

Next, we'll select our scopes (identify and connections) in the URL generator:

A lot of discord OAuth scope selectors, underneath the headline of

And at last, just select the redirect you entered previously and your URL will be shown below:

A select field showing the redirect url http://localhost:8080/discord/redirect/oauth

When you provide a user with this link, they will be prompted with this popup. After clicking on authorize they will then be redirected to your page, allowing us to start using OAuth.

A form showing that an application wants to access identification and connection information from the user, prompting them to cancel or authorize it

Creating a simple WebServer

Oauth requests from Discord work in the following way:

  1. User authorizes through your OAuth link
  2. Discord redirects them to your WebServer with an authorization code attached
  3. You send a request to discord to get an authorization token
  4. You query data using the token

It sounds really simple and it is really simple, but it took way to long for me to understand this, so let's go through it step by step. We firstly create a simple WebServer for our OAuth redirect:

embeddedServer(Netty, 8080) {
    routing { 
        get("discord/redirect/oauth") {

        }
    }
}.start(true)
Enter fullscreen mode Exit fullscreen mode

After that, we just have to search for the authorization code in the request parameters and maybe add a small response text (this code is in the get... section):

call.respondText("The redirect was successful, you can close this page now")
val authorizationCode = call.request.queryParameters["code"]
Enter fullscreen mode Exit fullscreen mode

Making the api requests

After receiving our authorization code, we still need to get the authorization token, to actually start querying user data. We'll start by creating a json decoder plus an Http-Client for our request:

val json = Json {
    ignoreUnknownKeys = true
}

val client = HttpClient(CIO) {
    install(ClientContentNegotiation) {
    (json)
        formData()
    }
}
Enter fullscreen mode Exit fullscreen mode

Now we need a data class that our json decoder can use for the serialization process (for other languages, use the best json decoding manner available):

@Serializable
data class DiscordTokenResponse(
    @SerialName("access_token")
    val accessToken: String,
    @SerialName("token_type")
    val tokenType: String,
    @SerialName("expires_in")
    val expiresIn: Long,
    @SerialName("refresh_token")
    val refreshToken: String,
    @SerialName("scope")
    val scope: String
)
Enter fullscreen mode Exit fullscreen mode

Then we'll create our request and send it using our created client:

val tokenCall = client.post("https://discord.com/api/oauth2/token") {
    setBody(FormDataContent(Parameters.build {
        append("client_id", clientId)
        append("client_secret", clientSecret)
        append("grant_type", "authorization_code")
        append("code", authorizationCode)
        append("redirect_uri", uri)
    }))
}
Enter fullscreen mode Exit fullscreen mode

At last we'll check for the request's success and then serialize our json output:

if (!tokenCall.status.isSuccess()) return
val tokenResponse = json.decodeFromString<DiscordTokenResponse>(tokenCall.bodyAsText())
Enter fullscreen mode Exit fullscreen mode

Now we have our lovely token. We can now use this token to firstly get our userId and a corresponding data class:

@Serializable
data class DiscordUser(
    val id: String,
    val username: String,
)
Enter fullscreen mode Exit fullscreen mode
val userCall = client.get("https://discord.com/api/users/@me") {
    header("Authorization", "Bearer ${tokenResponse.accessToken}")
}

if (!userCall.status.isSuccess()) return
val discordUser = json.decodeFromString<DiscordUser>(userCall.bodyAsText())
Enter fullscreen mode Exit fullscreen mode

Then we follow up with our connection request and the corresponding data class:

@Serializable
data class DiscordConnection(
    val id: String,
    val name: String,
    val type: String,
    @SerialName("friend_sync")
    val friendSync: Boolean,
    @SerialName("metadata_visibility")
    val metadataVisibility: Int,
    @SerialName("show_activity")
    val showActivity: Boolean,
    @SerialName("two_way_link")
    val twoWayLink: Boolean,
    val verified: Boolean,
    val visibility: Int
)
Enter fullscreen mode Exit fullscreen mode
val connectionCall = client.get("https://discord.com/api/users/@me/connections") {
    header("Authorization", "Bearer ${tokenResponse.accessToken}")
}

if (!connectionCall.status.isSuccess()) return

val connections = json.decodeFromString<List<DiscordConnection>>(connectionCall.bodyAsText())
Enter fullscreen mode Exit fullscreen mode

Now that we have our values, we can simply connect them, completing our little OAuth adventure.

Conclusion

Discord OAuth, or OAuth in general does take a bit to get used to, but isn't as complicated as I once thought. I had a lot of problems with it, when I first tried to use it and needed help by other developers to do so. But after a lot of time and stress I finally understood it. The thing I learned from this, you just have to try over and over until you succeed.

This was more of a tutorial article, hope that it was still somewhat entertaining and informational. The examples are in Kotlin, but the general structure can be applied to all languages.

Sources

Top comments (0)