在HTML5 Web App中使用OAuth2

Cam*_*ron 47 javascript oauth-2.0

我目前正在尝试使用OAuth2开发一个完全用JavaScript构建的移动应用程序,该应用程序与CakePHP API进行通信.看看下面的代码,看看我的应用程序当前的外观(请注意,这是一个实验,因此代码混乱,缺乏结构区域等等.)

var access_token,
     refresh_token;

var App = {
    init: function() {
        $(document).ready(function(){
            Users.checkAuthenticated();
        });
    }(),
    splash: function() {
        var contentLogin = '<input id="Username" type="text"> <input id="Password" type="password"> <button id="login">Log in</button>';
        $('#app').html(contentLogin);
    },
    home: function() {  
        var contentHome = '<h1>Welcome</h1> <a id="logout">Log out</a>';
        $('#app').html(contentHome);
    }
};

var Users = {
    init: function(){
        $(document).ready(function() {
            $('#login').live('click', function(e){
                e.preventDefault();
                Users.login();
            }); 
            $('#logout').live('click', function(e){
                e.preventDefault();
                Users.logout();
            });
        });
    }(),
    checkAuthenticated: function() {
        access_token = window.localStorage.getItem('access_token');
        if( access_token == null ) {
            App.splash();
        }
        else {
            Users.checkTokenValid(access_token);
        }
    },
    checkTokenValid: function(access_token){

        $.ajax({
            type: 'GET',
            url: 'http://domain.com/api/oauth/userinfo',
            data: {
                access_token: access_token
            },
            dataType: 'jsonp',
            success: function(data) {
                console.log('success');
                if( data.error ) {
                    refresh_token = window.localStorage.getItem('refresh_token');
                     if( refresh_token == null ) {
                         App.splash();
                     } else {
                         Users.refreshToken(refresh_token);
                    }
                } else {
                    App.home();
                }
            },
            error: function(a,b,c) {
                console.log('error');
                console.log(a,b,c);
                refresh_token = window.localStorage.getItem('refresh_token');
                 if( refresh_token == null ) {
                     App.splash();
                 } else {
                     Users.refreshToken(refresh_token);
                }
            }
        });

    },
    refreshToken: function(refreshToken){

        $.ajax({
            type: 'GET',
            url: 'http://domain.com/api/oauth/token',
            data: {
                grant_type: 'refresh_token',
                refresh_token: refreshToken,
                client_id: 'NTEzN2FjNzZlYzU4ZGM2'
            },
            dataType: 'jsonp',
            success: function(data) {
                if( data.error ) {
                    alert(data.error);
                } else {
                    window.localStorage.setItem('access_token', data.access_token);
                    window.localStorage.setItem('refresh_token', data.refresh_token);
                    access_token = window.localStorage.getItem('access_token');
                    refresh_token = window.localStorage.getItem('refresh_token');
                    App.home();
                }
            },
            error: function(a,b,c) {
                console.log(a,b,c);
            }
        });

    },
    login: function() {
        $.ajax({
            type: 'GET',
            url: 'http://domain.com/api/oauth/token',
            data: {
                grant_type: 'password',
                username: $('#Username').val(),
                password: $('#Password').val(),
                client_id: 'NTEzN2FjNzZlYzU4ZGM2'
            },
            dataType: 'jsonp',
            success: function(data) {
                if( data.error ) {
                    alert(data.error);
                } else {
                    window.localStorage.setItem('access_token', data.access_token);
                    window.localStorage.setItem('refresh_token', data.refresh_token);
                    access_token = window.localStorage.getItem('access_token');
                    refresh_token = window.localStorage.getItem('refresh_token');
                    App.home();
                }
            },
            error: function(a,b,c) {
                console.log(a,b,c);
            }
        });
    },
    logout: function() {
        localStorage.removeItem('access_token');
        localStorage.removeItem('refresh_token');
        access_token = window.localStorage.getItem('access_token');
        refresh_token = window.localStorage.getItem('refresh_token');
        App.splash();
    }
};
Run Code Online (Sandbox Code Playgroud)

我有很多与OAuth实施有关的问题:

1.)显然将access_token存储在localStorage中是不好的做法,我应该使用cookie.有谁能解释为什么?因为这不是更安全或更不安全,因为cookie数据不会被加密.

更新:根据这个问题:本地存储与存储localStorage中的数据的Cookie仅在客户端可用,并且不像cookie那样做任何HTTP请求,所以对我来说似乎更安全,或者至少似乎没有据我所知,有任何问题!

2.)与问题1相关,使用cookie到期时间对我来说同样毫无意义,就好像你查看代码一样,在app上发出请求以获取用户信息,如果查询,则会返回错误它已在服务器端过期,并需要refresh_token.所以不确定在BOTH客户端和服务器上有到期时间的好处,当服务器真正重要时.

3.)如何在没有A的情况下获取刷新令牌,将其与原始access_token一起存储以供以后使用,以及B)还存储client_id?我被告知这是一个安全问题,但我怎样才能在以后使用它们,但是在仅限JS的应用程序中保护它们?再次看到上面的代码,看看到目前为止我是如何实现的.

jan*_*sen 82

看起来您正在使用资源所有者密码凭据 OAuth 2.0流程,例如,提交用户名/密码以获取访问令牌和刷新令牌.

  • 访问令牌在JavaScript暴露的访问令牌被以某种方式暴露的风险是由它的寿命短缓解.
  • 刷新令牌不应暴露于客户端的JavaScript.它用于获取更多访问令牌(正如您上面所做的那样)但是如果攻击者能够获得刷新令牌,他们将能够随意获得更多访问权限,直到OAuth服务器撤销授权为止.发出刷新令牌的客户端.

考虑到这一背景,让我来解答你的问题:

  1. cookie或localstorage将在页面刷新时为您提供本地持久性.将访问令牌存储在本地存储中可以为CSRF攻击提供更多保护,因为它不会像cookie那样自动发送到服务器.您的客户端javascript需要将其从localstorage中拉出来并在每个请求中传输它.我正在开发一个OAuth 2应用程序,因为这是一个单页方法我不做; 相反,我只是把它留在记忆中.
  2. 我同意......如果你在cookie中存储它只是为了持久性而不是到期,服务器将在令牌过期时响应错误.我认为您可能创建一个过期的cookie的唯一原因是,您可以检测它是否已过期而不首先发出请求并等待错误响应.当然,通过保存已知的到期时间,您可以对本地存储执行相同的操作.
  3. 这是我相信的整个问题的关键......"如何获得刷新令牌,没有A,将其与原始access_token一起存储以便稍后使用,以及B)还存储client_id".不幸的是,你真的不能......正如在介绍性评论中所指出的那样,让刷新令牌客户端否定访问令牌的有限生命周期所提供的安全性.我在我的应用程序中做的事情(我没有使用任何持久的服务器端会话状态)如下:
    • 用户将用户名和密码提交给服务器
    • 然后,服务器将用户名和密码转发到OAuth端点(在上面的示例中)http://domain.com/api/oauth/token,并接收访问令牌和刷新令牌.
    • 服务器加密刷新令牌并将其设置在cookie中(应该只是HTTP)
    • 服务器仅以明文形式(在JSON响应中)和加密的仅HTTP cookie响应访问令牌
    • 客户端javascript现在可以读取并使用访问令牌(存储在本地存储或其他任何内容中)
    • 当访问令牌到期时,客户端向服务器(不是OAuth服务器,而是托管应用程序的服务器)提交请求以获取新令牌
    • 服务器接收它创建的加密的HTTP cookie,解密它以获取刷新令牌,请求新的访问令牌,最后在响应中返回新的访问令牌.

不可否认,这确实违反了您所寻求的"仅限JS"约束.但是,a)你真的不应该在javascript中有一个刷新令牌,而b)它在登录/注销时需要非常小的服务器端逻辑,并且没有持久的服务器端存储.

关于CSRF的说明:如评论中所述,此解决方案不涉及跨站点请求伪造 ; 请参阅OWASP CSRF预防备忘单,了解有关解决这些攻击形式的更多想法.

另一个替代方法是根本不要求刷新令牌(不确定这是否是您正在处理的OAuth 2实现的选项;刷新令牌根据规范是可选)并在其到期时不断重新进行身份验证.

希望有所帮助!

  • 我不明白如何加密refresh_token提供任何额外的安全性,因为您的服务器将愉快地解密它,转发它并将新的访问令牌返回给用户.如果有人以某种方式获取持久加密的cookie,是什么阻止他们使用您的服务器无限期地获取API的访问权限?您是否认为值得通过服务器进行额外的复杂化以避免明确刷新令牌? (14认同)
  • 在仅限http的cookie中刷新令牌是否会为应用程序购买针对XSS的一些保护但是将其暴露给CSRF?如果我可以欺骗用户点击然后发布表单的恶意链接,我现在拥有带有加密刷新令牌的cookie.使用该cookie,我是否能够在您的应用程序中转到端点(倒数第二步)并获取访问令牌? (4认同)

rsn*_*ell 3

完全安全的唯一方法是不存储客户端的访问令牌。任何能够(物理)访问您的浏览器的人都可以获取您的令牌。

  1. 您对这两者都不是一个好的解决方案的评估是准确的。

  2. 如果您仅限于客户端开发,那么使用过期时间是最好的选择。它不需要您的用户频繁地使用 Oauth 重新进行身份验证,并保证令牌不会永远存在。仍然不是最安全的。

  3. 获取新令牌需要执行 Oauth 工作流程来获取新令牌。client_id 与 Oauth 运行的特定域绑定。

保留 Oauth 令牌的最安全方法是服务器端实现。

  • 我想您会发现“仅限客户端的应用程序”仍然利用一些服务器端代理来确保安全。请参阅本文作为一个示例:http://derek.io/blog/2010/how-to-secure-oauth-in-javascript/ (2认同)