OAuth2入門

OAuth2を使うと、Discord APIの認証機能を利用したり、APIからデータを取得したりするアプリケーションを開発することができます。 例えば、ユーザー情報を表示するWebダッシュボードのようなものを作ったり、TwitchやSteamといった外部サイトのアカウントと連携したアカウントを取得できたり、ユーザーが参加しているサーバーの情報を、ボットがそのサーバーにいなくても取得できたりします。 OAuth2を正しく使えば、ボットの機能を大きく拡張できるということです。

簡単な例

基本的なWebサーバーの設定

ほとんどの場合、WebサイトはOAuth2を使って外部サービスからユーザに関する情報を取得します。 以下の例では、 expressopen in new window を使用して、ユーザーのDiscordの情報を基にあいさつができるWebサーバーを開発していきます。 まずは config.json, index.js, index.html という3つのファイルを作成します。

config.json を編集します。このファイルは、クライアントIDやクライアントシークレット、サーバーのポート番号を保存するために使います。

{
    "clientId": "",
    "clientSecret": "",
    "port": 53134
}
1
2
3
4
5

index.js はサーバーの起動とリクエストの処理に使用されます。 index ページ (/) にアクセスすると、サーバーはレスポンスとしてHTMLファイルを送信します。

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 は、ログインした際にUIとOAuth情報を表示するために使われます。

<!DOCTYPE html>
<html>
<head>
    <title>Discord OAuth2アプリ</title>
</head>
<body>
    <div id="info">
        やあ!
    </div>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11

npm i expressを実行したら、 node index.js でサーバーを起動できます。 起動したあと、http://localhost:53134にアクセスすると、画面には"やあ!"と表示されるはずです。

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

OAuth2のURLを取得する

Webサーバーを起動できたので、Discordから情報を取得してみましょう。 まずは Discord Applicationsopen in new windowにアクセスします。そして、アプリケーションを新規作成するか、既存のものを選択したあと、「OAuth2」ページに切り替えてください。

OAuth2 application page

このとき、client idclient secret をメモしてください。 そして、config.json ファイルに値をコピーアンドペーストします。あとで必要になります。 リダイレクト URLには、とりあえず http://localhost:53134 を追加しておいてください。

Adding Redirects

リダイレクト URL を追加したら、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
25
26

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 node-fetchopen in new window to make requests to Discord; you can install it with npm i node-fetch.

Require node-fetch and make your request.

const fetch = require('node-fetch');
const express = require('express');
const { clientId, clientSecret, port } = require('./config.json');

const app = express();

app.get('/', async ({ query }, response) => {
    const { code } = query;

    if (code) {
        try {
            const oauthResult = await fetch('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 oauthResult.json();
            console.log(oauthData);
        } catch (error) {
            // NOTE: An unauthorized token will not throw an error;
            // it will return a 401 Unauthorized response in the try block above
            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

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 oauthData = await oauthResult.json();

const userResult = await fetch('https://discord.com/api/users/@me', {
    headers: {
        authorization: `${oauthData.token_type} ${oauthData.access_token}`,
    },
});

console.log(await userResult.json());


 
 
 
 
 

 
1
2
3
4
5
6
7
8
9

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.

Last Updated: 2022/4/19 3:48:55