mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-15 11:03:30 +01:00
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:
BIN
apps/guide/content/docs/legacy/oauth2/images/add-redirects.png
Normal file
BIN
apps/guide/content/docs/legacy/oauth2/images/add-redirects.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
BIN
apps/guide/content/docs/legacy/oauth2/images/generate-url.png
Normal file
BIN
apps/guide/content/docs/legacy/oauth2/images/generate-url.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
BIN
apps/guide/content/docs/legacy/oauth2/images/oauth2-app-page.png
Normal file
BIN
apps/guide/content/docs/legacy/oauth2/images/oauth2-app-page.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
302
apps/guide/content/docs/legacy/oauth2/oauth2.mdx
Normal file
302
apps/guide/content/docs/legacy/oauth2/oauth2.mdx
Normal 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.
|
||||
|
||||

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

|
||||
|
||||
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.
|
||||
|
||||

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

|
||||
|
||||
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)
|
||||
Reference in New Issue
Block a user