Getting started with 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
open in new window 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.
{
"clientId": "",
"clientSecret": "",
"port": 53134
}
2
3
4
5
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.
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}`));
2
3
4
5
6
7
8
9
10
index.html
will be used to display the user interface and OAuth data once logged in.
<!DOCTYPE html>
<html>
<head>
<title>My Discord OAuth2 App</title>
</head>
<body>
<div id="info">Hoi!</div>
</body>
</html>
2
3
4
5
6
7
8
9
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!".
TIP
Although we're using express, there are many other alternatives to handle a web server, such as: fastifyopen in new window, koaopen in new window, and the native Node.js http moduleopen in new window.
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 applicationsopen in new window, 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 hereopen in new window.
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 SPAsopen in new window, 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 identifieropen in new window 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
open in new window 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 "#".
<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>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
endpointopen in new window is a user objectopen in new window and should look something like this:
{
"id": "123456789012345678",
"username": "User",
"discriminator": "0001",
"avatar": "1cc0a3b14aec3499632225c708451d67",
...
}
2
3
4
5
6
7
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 CSRFopen in new window 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.
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');
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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.
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!');
}
2
3
4
5
6
7
8
9
10
TIP
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, 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.
app.get('/', (request, response) => {
console.log(`The access code is: ${request.query.code}`);
return response.sendFile('index.html', { root: '.' });
});
2
3
4
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 applicationsopen in new window and get them. You can use undici
open in new window to make requests to Discord.
To install undici, run the following command:
npm install undici
yarn add undici
pnpm add undici
Require undici
and make your request.
TIP
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.
const { request } = require('undici');
const express = require('express');
const { clientId, clientSecret, port } = require('./config.json');
async function getJSONResponse(body) {
let fullBody = '';
for await (const data of body) {
fullBody += data.toString();
}
return JSON.parse(fullBody);
}
const app = express();
app.get('/', async ({ query }, response) => {
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',
}),
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
});
const oauthData = await getJSONResponse(tokenResponseData.body);
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: '.' });
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
WARNING
The content-type for the token URL must be application/x-www-form-urlencoded
, which is why URLSearchParams
is used.
Now try visiting your OAuth2 URL and authorizing your application. Once you're redirected, you should see an access token responseopen in new window in your console.
{
"access_token": "an access token",
"token_type": "Bearer",
"expires_in": 604800,
"refresh_token": "a refresh token",
"scope": "identify"
}
2
3
4
5
6
7
With an access token and a refresh token, you can once again use the /api/users/@me
endpointopen in new window to fetch the user objectopen in new window.
const userResult = await request('https://discord.com/api/users/@me', {
headers: {
authorization: `${oauthData.token_type} ${oauthData.access_token}`,
},
});
console.log(await getJSONResponse(userResult.body));
2
3
4
5
6
7
TIP
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 6759open in new window
Discord Docs for OAuth2open in new window
Resulting code
If you want to compare your code to the code we've constructed so far, you can review it over on the GitHub repository here open in new window.