如何安全地存储Discord(OAuth2)用户的访问令牌?

时间:2019-01-20 10:16:28

标签: javascript node.js cookies oauth-2.0 discord

我正在努力寻找一种安全地保存访问令牌的方法,该访问令牌是我的Web应用程序在用户授权应用程序后从DiscordAPI检索到的。

我正在为Discord Bot创建一个Web界面。在此重要的是,并非每个人都可以使用它。仅允许特定Discord服务器上的服务器主持人等访问网站的大部分内容。为此,我使用了来自Discord的OAuth2资料来检索访问令牌,通过该令牌我可以获取用户信息,例如其唯一ID。 然后使用该ID来检查他们在Discord服务器上所扮演的角色。

现在,获取访问令牌的部分已编写脚本,并且似乎可以正常工作。我可以使用令牌从Discord API等中查询数据。

我主要关心的是这里的安全性。我已经阅读了几篇不同的文章,但是每个人似乎对此都有不同的看法。

我经常读到的一个“解决方案”(甚至在Auth2网站上)是将令牌保存在cookie中。

在这里,我不确定这是否完全安全。我不能仅将令牌仅存储在服务器上,因为用户必须在令牌生存期内保持登录状态。但是,将其存储为cookie会使它容易遭受攻击(我对此尚不熟悉,无法防范)。

我目前在收到令牌时有以下一行:

res.cookie('authToken', response.access_token);

我还将行调整为以下内容,因为这样做是为了删除通过脚本读取cookie的所有方式(足够吗?):

res.cookie('authToken', response.access_token, { httpOnly: true });

当访问Web界面的其他部分时,我检查cookie是否存在,并尝试向Discord询问有关它的用户信息。如果用户信息正确返回,则认为用户已通过正确的身份验证:

router.get('/', catchAsync(async (req, res, next) => {  
    if(req.cookies.authToken === undefined) {

       // Render index view
       res.render('index', { 
           authenticated: false
       });
   }
   else {
        // Grab the token
        const localToken = req.cookies.authToken;
        // Use the token to query user information
        const response = await discordapi.GetDiscordUserDetails(localToken);
        if(response.id) {
            // Open the Index page and pass the username over
            res.render('index', {
                authenticated: true,
                username: response.username,
                userid: response.id,
                avatarid: response.avatar
            });
        } else {
            res.render('index', { 
                authenticated: false
            });
        }
   }
}));

(我传递的“经过身份验证的”布尔值仅更改了html(快速车把)文档中登录按钮的可见性。您无法对其进行任何其他操作。)

现在,由于我对此知识的了解有限,我几乎认为这是最糟糕的方法,我想对此加以改进。

另一种解决方案是将访问令牌存储在Web服务器本身的数据库中。因此,基本上根本不会以任何方式将其显示给用户。 但是,我需要一种将该令牌与用户匹配的方式,因此他们仍然需要某种cookie,其中包含我可以用来获取令牌的信息。 我认为,如果令牌将在数据库中加密并且用户具有解密它的密钥,那将是最佳选择。 但是,我不确定这是否再次安全,因为如果您读出cookie,您仍然可以访问令牌,或者至少表现得像特定用户一样。

那么保持Cookie 100%安全是唯一的方法吗?我对此很陌生,即使这很可能只是一个简单的不和谐服务器的一个小型Web界面,我仍然希望正确执行此操作。

我还读到,将令牌作为某种密码来处理并将其包装到另一层“会话”中是一种方法,但是听起来很复杂,实际上我不知道从哪里开始。 / p>

我希望有人能对此有所了解,因为安全是我真正担心的事情。

感谢您的时间!

1 个答案:

答案 0 :(得分:1)

  • 无聊的故事:

首先,我希望看到网上银行应用程序以纯文本形式发送身份验证令牌,并在POST中附加为查询字符串。

话虽这么说,开发人员必须做出的大多数安全考虑都必须旨在保护后端结构,而不是防止用户被客户端黑客。 (显然,您将尽一切可能使最后一个假设变得不太合理,但从道德和技术上讲,这是一个限制。我的意思是,如果我连接到一个非安全网络,并且有人拦截了我的通信并设法对其进行解码,我认为这将是我的错,而不是别人。)

无论如何,我将不再是哲学家,但是最后一次已知的观察将永远不会为任何问题提供100%安全的解决方案。

  • 真正的答案:

使用httpOnly cookie是传输和存储身份验证令牌的最简单,最安全的方法,但是如果这还不够的话,还可以实现其他一些安全层。这只是一些想法,可能还会更多!

  1. 减少令牌的生存时间,并在一段时间不活动之后关闭服务器端的会话。您将必须记录活动会话,每个会话都包含其开始时间和活动令牌等。

  2. IP检查。如果会话以美国的IP地址开始,而五分钟后该IP地址似乎来自菲律宾,那么您可能必须采取一些措施。

  3. 使用外部身份验证服务,例如AWS Cognito。但这无能为力,您无法独自完成。

  4. 实施多重身份验证。

  5. 类似于IP检查,您可以使用用户代理字符串作为种子来计算哈希并将其存储。如果您对客户身份有疑问,请进行检查。

  6. 谈论哈希,您可以存储令牌并将哈希发送给用户。唯一的改进是,它可以防止某人直接调用Discord API(如果该API没有任何过滤器)。例如,密码始终存储为哈希。

  7. 上一个的任何组合。

该列表可能会无限增加,在我看来,经过一段时间后,您将只会不必要地浪费时间和资源。只需记住在URL中发送令牌的银行,您就会感觉好多了。

  • 额外:

我最近写了这个JavaScript函数来生成会话ID。如果您认为它有用,请随意对其进行修改。

使用大小定义输出长度,然后键入以下内容:

  1. 'alpha'
  2. 'num'
  3. “字母数字”
  4. “特殊”
  5. “独特”
function randomID(size, type) {

    // SEEDS
    const special = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz~`!@#$%^&*()_-+={[}]|:;<,>.?/0123456789";
    const alphaNum = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
    const alpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
    const num = "0123456789";

    let x, y, z;
    let currentIndex;
    let randomIndex;
    let temporaryValue;

    let i;

    if (type === 'special') {
        x = special;
    } else if (type === 'alphaNum' || type === 'unique') {
        x = alphaNum;
    } else if (type === 'alpha') {
        x = alpha;
    } else {
        x = num;
    }

    x = x.split();
    currentIndex = x.length;

    // WHILE THERE ARE ELEMENTS TO SHUFFLE
    while (currentIndex !== 0) {
        // PICK A REMAINING ELEMENT
        randomIndex = Math.floor(Math.random() * currentIndex);
        currentIndex--;
        // AND SWAP IT WITH THE CURRENT ELEMENT
        temporaryValue = x[currentIndex];
        x[currentIndex] = x[randomIndex];
        x[randomIndex] = temporaryValue;
    }
    x = x.toString();

    y = '';
    if (type === 'unique') {
        z = new Date();
        y += x.charAt(x.length - 1 - z.getDate());
        y += x.charAt(x.length - 1 - z.getHours());
        y += z.getFullYear().toString();
        y += x.charAt(x.length - 1 - z.getMonth());
        y += x.charAt(x.length - 1 - z.getMinutes());
        y += x.charAt(x.length - 1 - z.getSeconds());
        y = y.split('').reverse().join('');
        y += '-';
    }

    for (i = 0; i < size; i++) {
        y += x.charAt(Math.floor(Math.random() * x.length));
    }

    return y;

}