javascript - 在 HTML5 Web App 中使用 OAuth2

标签 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();
    }
};

我有一些与我的 OAuth 实现相关的问题:

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

更新:根据这个问题:Local Storage vs Cookies无论如何,将数据存储在 localStorage 中仅在客户端可用,并且不执行任何与 cookie 不同的 HTTP 请求,因此对我来说似乎更安全,或者至少据我所知似乎没有任何问题!

2.) 关于问题 1,使用 cookie 作为过期时间,对我来说同样毫无意义,就好像您查看代码一样,在应用程序启动时发出请求以获取用户信息,如果它在服务器端已过期,需要 refresh_token。所以不确定在客户端和服务器上都有到期时间的好处,当服务器才是真正重要的时候。

3.) 如何在没有 A 的情况下获取刷新 token ,将其与原始 access_token 一起存储以备后用,以及 B) 还存储一个 client_id?有人告诉我这是一个安全问题,但我以后如何使用这些,但在仅 JS 的应用程序中保护它们?再次查看上面的代码以了解到目前为止我是如何实现的。

最佳答案

看起来您正在使用 Resource Owner Password Credentials OAuth 2.0 流程,例如提交用户名/通行证以获取访问 token 和刷新 token 。

  • 访问 token 可以在 javascript 中公开,访问 token 以某种方式公开的风险通过其较短的生命周期得到缓解。
  • 刷新 token 不应暴露给客户端 javascript。它用于获取更多访问 token (如您在上面所做的那样)但如果攻击者能够获得刷新 token ,他们将能够随意获得更多访问 token ,直到 OAuth 服务器撤销授权 的客户刷新 token 发出。

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

  • 诚然,这确实违反了您正在寻找的“仅 JS”约束。但是,a) 同样,您真的不应该在 javascript 中使用刷新 token ,并且 b) 它在登录/注销时需要非常少的服务器端逻辑,并且不需要持久的服务器端存储。

    关于CSRF的说明 :如评论中所述,此解决方案未解决 Cross-site Request Forgery ;见 OWASP CSRF Prevention Cheat Sheet获取有关解决这些攻击形式的进一步想法。

    另一种选择是根本不请求刷新 token (不确定这是否是您正在处理的 OAuth 2 实现的一个选项;刷新 token 是可选的 per the spec )并在它到期时不断重新进行身份验证。

    希望有帮助!

    关于javascript - 在 HTML5 Web App 中使用 OAuth2,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/18280827/

    相关文章:

    已知所有边, Angular 和前两个点时查找三 Angular 形第三点的Javascript函数

    javascript - JS : Why "Strict Mode " is not showing Reference Error while declaring a variable without var while keeping function and variable name same

    .net - 在 Azure 中保护应用程序的客户端 ID 和 key

    authentication - OAuth2 不同的客户端认证方式

    oauth-2.0 - 没有浏览器的 OAuth2?

    spring - ClassNotFoundException:SimpleGrantedAuthority-Grails 2.4.2和Spring Security

    javascript - AngularJS动态更新指令链接函数中的属性

    javascript - 构建一个简单的随机发生器,其中包括一个点、一个下划线、字母 A 和字母 B

    javascript - 第二次单击时状态未更改时组件重新呈现

    ruby-on-rails - Spring和中间件冲突?