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 expressopen 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
}
1
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}`));
1
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>
1
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.

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

Adding Redirects

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

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:

Authorization Page

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



 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
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",
	...
}
1
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');
	}
};
 
 
 
 
 
 
 
 
 
 




 
 
 
 



1
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!');
}

 





 
 
 
1
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: '.' });
});

 


1
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 undiciopen 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: '.' });
});
 

 

 
 
 
 
 
 
 
 
 
 



 
 

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
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"
}
1
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));
 
 
 
 
 

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