feat(guide): port legacy guide (#10938)

* feat: initial attempt at porting legacy guide

* feat: completion of legacy guide backport

* chore: lockfile shenanigans

* fix: handle svgs

* fix: replace svg with mermaid integration

* chore: format

* chore: remove unnecssary bullet

* chore: cleanup code highlights

* chore: explicit return

* chore: move display components after interactive components in sidebar

* chore: voice

* top link should be installation
* add docs link to sidebar

* feat: subguide-based accent styles

* chore: don't list faq twice

* chore: mention display components in interactive components

* fix: remove unoccs/order rule from guide

* chore: redirect to legacy guide instead of /guide root

* refactor: use `<kbd>`

* refactor: more kbd use

* Update apps/guide/content/docs/legacy/app-creation/handling-events.mdx

Co-authored-by: Naiyar <137700126+imnaiyar@users.noreply.github.com>

* chore: fix typos

Co-authored-by: Qjuh <76154676+Qjuh@users.noreply.github.com>

* chore: fix typos

* chore: fix links regarding secret stores across coding platforms

* chore: fix typo

* chore: link node method directly

Co-authored-by: Qjuh <76154676+Qjuh@users.noreply.github.com>

* chore: typos

Co-authored-by: Vlad Frangu <me@vladfrangu.dev>

* chore: typo

Co-authored-by: Vlad Frangu <me@vladfrangu.dev>

* fix: prevent v14 changes from being listed twice

* chore: prefer relative links

* chore: missed link conversion

* chore: missed link conversion

* chore: fix link

* chore: remove legacy code highlight markers

* chore: rephrase and extend contributing guidelines

* feat(setup): suggest cli flag over dotenv package

* chore: move introduction in sidebar

better navigation experience if the 'next page' in intro refers to getting started vs. updating/faq

* fix: replace outdated link

* fix: update voice dependencies

* chore: update node install instructions

* fix: list in missing access callout

* chore: match bun env file format

* chore: restore ffmpeg disclaimer

* fix: lockfile conflict

* chore: action row typo

Co-authored-by: Vlad Frangu <me@vladfrangu.dev>

* chore: no longer use at-next for pino

---------

Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
Co-authored-by: Qjuh <76154676+Qjuh@users.noreply.github.com>
Co-authored-by: Naiyar <137700126+imnaiyar@users.noreply.github.com>
Co-authored-by: Vlad Frangu <me@vladfrangu.dev>
This commit is contained in:
Souji
2025-07-08 15:01:50 +02:00
committed by GitHub
parent ee3ca6f7c6
commit bc6005f446
136 changed files with 11847 additions and 48 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

View File

@@ -0,0 +1,302 @@
---
title: OAuth2
---
OAuth2 enables application developers to build applications that utilize authentication and data from the Discord API. Developers can use this to create things such as web dashboards to display user info, fetch linked third-party accounts like Twitch or Steam, access users' guild information without actually being in the guild, and much more. OAuth2 can significantly extend the functionality of your bot if used correctly.
## A quick example
### Setting up a basic web server
Most of the time, websites use OAuth2 to get information about their users from an external service. In this example, we will use [`express`](https://expressjs.com/) to create a web server to use a user's Discord information to greet them. Start by creating three files: `config.json`, `index.js`, and `index.html`.
`config.json` will be used to store the client ID, client secret, and server port.
```json title="config.json" lineNumbers
{
"clientId": "",
"clientSecret": "",
"port": 53134
}
```
`index.js` will be used to start the server and handle requests. When someone visits the index page (`/`), an HTML file will be sent in response.
```js title="index.js" lineNumbers
const express = require('express');
const { port } = require('./config.json');
const app = express();
app.get('/', (request, response) => {
return response.sendFile('index.html', { root: '.' });
});
app.listen(port, () => console.log(`App listening at http://localhost:${port}`));
```
`index.html` will be used to display the user interface and OAuth data once logged in.
```html title="index.html" lineNumbers
<!DOCTYPE html>
<html>
<head>
<title>My Discord OAuth2 App</title>
</head>
<body>
<div id="info">Hoi!</div>
</body>
</html>
```
After running `npm i express`, you can start your server with `node index.js`. Once started, connect to `http://localhost:53134`, and you should see "Hoi!".
<Callout>
Although we're using express, there are many other alternatives to handle a web server, such as:
[fastify](https://www.fastify.io/), [koa](https://koajs.com/), and the [native Node.js http
module](https://nodejs.org/api/http.html).
</Callout>
### Getting an OAuth2 URL
Now that you have a web server up and running, it's time to get some information from Discord. Open [your Discord applications](https://discord.com/developers/applications/), create or select an application, and head over to the "OAuth2" page.
![OAuth2 application page](./images/oauth2-app-page.png)
Take note of the `client id` and `client secret` fields. Copy these values into your `config.json` file; you'll need them later. For now, add a redirect URL to `http://localhost:53134` like so:
![Adding Redirects](./images/add-redirects.png)
Once you've added your redirect URL, you will want to generate an OAuth2 URL. Lower down on the page, you can conveniently find an OAuth2 URL Generator provided by Discord. Use this to create a URL for yourself with the `identify` scope.
![Generate an OAuth2 URL](./images/generate-url.png)
The `identify` scope will allow your application to get basic user information from Discord. You can find a list of all scopes [here](https://discord.com/developers/docs/topics/oauth2#shared-resources-oauth2-scopes).
### Implicit grant flow
You have your website, and you have a URL. Now you need to use those two things to get an access token. For basic applications like [SPAs](https://en.wikipedia.org/wiki/Single-page_application), getting an access token directly is enough. You can do so by changing the `response_type` in the URL to `token`. However, this means you will not get a refresh token, which means the user will have to explicitly re-authorize when this access token has expired.
After you change the `response_type`, you can test the URL right away. Visiting it in your browser, you will be directed to a page that looks like this:
![Authorization Page](./images/authorize-app-page.png)
You can see that by clicking `Authorize`, you allow the application to access your username and avatar. Once you click through, it will redirect you to your redirect URL with a [fragment identifier](https://en.wikipedia.org/wiki/Fragment_identifier) appended to it. You now have an access token and can make requests to Discord's API to get information on the user.
Modify `index.html` to add your OAuth2 URL and to take advantage of the access token if it exists. Even though [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) is for working with query strings, it can work here because the structure of the fragment follows that of a query string after removing the leading "#".
```html title="index.html" lineNumbers=11
<div id="info">Hoi!</div>
<a id="login" style="display: none;" href="your-oauth2-URL-here">Identify Yourself</a>
<script>
window.onload = () => {
const fragment = new URLSearchParams(window.location.hash.slice(1));
const [accessToken, tokenType] = [fragment.get('access_token'), fragment.get('token_type')];
if (!accessToken) {
return (document.getElementById('login').style.display = 'block');
}
fetch('https://discord.com/api/users/@me', {
headers: {
authorization: `${tokenType} ${accessToken}`,
},
})
.then((result) => result.json())
.then((response) => {
const { username, discriminator } = response;
document.getElementById('info').innerText += ` ${username}#${discriminator}`;
})
.catch(console.error);
};
</script>
```
Here you grab the access token and type from the URL if it's there and use it to get info on the user, which is then used to greet them. The response you get from the [`/api/users/@me` endpoint](https://discord.com/developers/docs/resources/user#get-current-user) is a [user object](https://discord.com/developers/docs/resources/user#user-object) and should look something like this:
```json
{
"id": "123456789012345678",
"username": "User",
"discriminator": "0001",
"avatar": "1cc0a3b14aec3499632225c708451d67",
...
}
```
In the following sections, we'll go over various details of Discord and OAuth2.
## More details
### The state parameter
OAuth2's protocols provide a `state` parameter, which Discord supports. This parameter helps prevent [CSRF](https://en.wikipedia.org/wiki/Cross-site_request_forgery) attacks and represents your application's state. The state should be generated per user and appended to the OAuth2 URL. For a basic example, you can use a randomly generated string encoded in Base64 as the state parameter.
```js
function generateRandomString() {
let randomString = '';
const randomNumber = Math.floor(Math.random() * 10);
for (let i = 0; i < 20 + randomNumber; i++) {
randomString += String.fromCharCode(33 + Math.floor(Math.random() * 94));
}
return randomString;
}
window.onload = () => {
// ...
if (!accessToken) {
const randomString = generateRandomString();
localStorage.setItem('oauth-state', randomString);
document.getElementById('login').href += `&state=${btoa(randomString)}`;
return (document.getElementById('login').style.display = 'block');
}
};
```
When you visit a URL with a `state` parameter appended to it and then click `Authorize`, you'll notice that after being redirected, the URL will also have the `state` parameter appended, which you should then check against what was stored. You can modify the script in your `index.html` file to handle this.
```html title="index.html"
<script>
// ... // [!code focus:15]
const fragment = new URLSearchParams(window.location.hash.slice(1));
const [accessToken, tokenType, state] = [
fragment.get('access_token'),
fragment.get('token_type'),
fragment.get('state'),
];
if (!accessToken) {
// ...
}
if (localStorage.getItem('oauth-state') !== atob(decodeURIComponent(state))) {
return console.log('You may have been clickjacked!');
}
</script>
```
<Callout type="error" title="Don't forgo security for a tiny bit of convenience!" />
### Authorization code grant flow
What you did in the quick example was go through the `implicit grant` flow, which passed the access token straight to the user's browser. This flow is great and simple, but you don't get to refresh the token without the user, and it is less secure than going through the `authorization code grant` flow. This flow involves receiving an access code, which your server then exchanges for an access token. Notice that this way, the access token never actually reaches the user throughout the process.
Unlike the [implicit grant flow](#implicit-grant-flow), you need an OAuth2 URL where the `response_type` is `code`. After you change the `response_type`, try visiting the link and authorizing your application. You should notice that instead of a hash, the redirect URL now has a single query parameter appended to it, i.e. `?code=ACCESS_CODE`. Modify your `index.js` file to access the parameter from the URL if it exists. In express, you can use the `request` parameter's `query` property.
```js
app.get('/', (request, response) => {
console.log(`The access code is: ${request.query.code}`);
return response.sendFile('index.html', { root: '.' });
});
```
Now you have to exchange this code with Discord for an access token. To do this, you need your `client_id` and `client_secret`. If you've forgotten these, head over to [your applications](https://discord.com/developers/applications) and get them. You can use [`undici`](https://www.npmjs.com/package/undici) to make requests to Discord.
To install undici, run the following command:
```sh tab="npm"
npm install undici
```
```sh tab="yarn"
yarn add undici
```
```sh tab="pnpm"
pnpm add undici
```
```sh tab="bun"
bun add undici
```
Require `undici` and make your request.
<Callout>
If you are used to the Fetch API and want to use that instead of how `undici` does it, instead of using
`undici#request`, use `undici#fetch` with the same parameters as node-fetch.
</Callout>
```js title="index.js"
const { request } = require('undici'); // [!code ++]
const express = require('express');
const { port } = require('./config.json'); // [!code --]
const { clientId, clientSecret, port } = require('./config.json'); // [!code ++]
const app = express();
app.get('/', async ({ query }, response) => {
// [!code ++:28]
const { code } = query;
if (code) {
try {
const tokenResponseData = await request('https://discord.com/api/oauth2/token', {
method: 'POST',
body: new URLSearchParams({
client_id: clientId,
client_secret: clientSecret,
code,
grant_type: 'authorization_code',
redirect_uri: `http://localhost:${port}`,
scope: 'identify',
}).toString(),
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
});
const oauthData = await tokenResponseData.body.json();
console.log(oauthData);
} catch (error) {
// NOTE: An unauthorized token will not throw an error
// tokenResponseData.statusCode will be 401
console.error(error);
}
}
return response.sendFile('index.html', { root: '.' });
});
```
<Callout>
The content-type for the token URL must be `application/x-www-form-urlencoded`, which is why `URLSearchParams` is
used.
</Callout>
Now try visiting your OAuth2 URL and authorizing your application. Once you're redirected, you should see an [access token response](https://discord.com/developers/docs/topics/oauth2#authorization-code-grant-access-token-response) in your console.
```json
{
"access_token": "an access token",
"token_type": "Bearer",
"expires_in": 604800,
"refresh_token": "a refresh token",
"scope": "identify"
}
```
With an access token and a refresh token, you can once again use the [`/api/users/@me` endpoint](https://discord.com/developers/docs/resources/user#get-current-user) to fetch the [user object](https://discord.com/developers/docs/resources/user#user-object).
```js
const userResult = await request('https://discord.com/api/users/@me', {
headers: {
authorization: `${oauthData.token_type} ${oauthData.access_token}`,
},
});
console.log(await userResult.body.json());
```
<Callout
type="error"
title="To maintain security, store the access token server-side but associate it with a session ID that you generate for the user."
/>
## Additional reading
[RFC 6759](https://tools.ietf.org/html/rfc6749)
[Discord Docs for OAuth2](https://discord.com/developers/docs/topics/oauth2)