在 HTML5 Web App 中使用 OAuth2 [英] Using OAuth2 in HTML5 Web App

查看:20
本文介绍了在 HTML5 Web App 中使用 OAuth2的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

var access_token,刷新令牌;无功应用 = {初始化:函数(){$(document).ready(function(){Users.checkAuthenticated();});}(),飞溅:函数(){var contentLogin = '<input id="密码" type="密码"><button id="登录">登录</button>';$('#app').html(contentLogin);},家:功能(){var contentHome = '<h1>欢迎</h1><a id="logout">注销</a>';$('#app').html(contentHome);}};var 用户 = {初始化:函数(){$(document).ready(function() {$('#login').live('click', function(e){e.preventDefault();用户登录();});$('#logout').live('click', function(e){e.preventDefault();用户注销();});});}(),checkAuthenticated:函数(){access_token = window.localStorage.getItem('access_token');if( access_token == null ) {App.splash();}别的 {Users.checkTokenValid(access_token);}},checkTokenValid: 函数(access_token){$.ajax({类型:'获取',url: 'http://domain.com/api/oauth/userinfo',数据: {访问令牌:访问令牌},数据类型:'jsonp',成功:功能(数据){console.log('成功');如果(数据.错误){refresh_token = window.localStorage.getItem('refresh_token');if(refresh_token == null ) {App.splash();} 别的 {Users.refreshToken(refresh_token);}} 别的 {App.home();}},错误:函数(a,b,c){console.log('错误');控制台日志(a,b,c);refresh_token = window.localStorage.getItem('refresh_token');if(refresh_token == null ) {App.splash();} 别的 {Users.refreshToken(refresh_token);}}});},刷新令牌:函数(刷新令牌){$.ajax({类型:'获取',url: 'http://domain.com/api/oauth/token',数据: {grant_type: 'refresh_token',刷新令牌:刷新令牌,client_id: 'NTezN2FjNzZlYzU4ZGM2'},数据类型:'jsonp',成功:功能(数据){如果(数据.错误){警报(数据错误);} 别的 {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();}},错误:函数(a,b,c){控制台日志(a,b,c);}});},登录:函数(){$.ajax({类型:'获取',url: 'http://domain.com/api/oauth/token',数据: {grant_type: '密码',用户名:$('#Username').val(),密码:$('#Password').val(),client_id: 'NTezN2FjNzZlYzU4ZGM2'},数据类型:'jsonp',成功:功能(数据){如果(数据.错误){警报(数据错误);} 别的 {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();}},错误:函数(a,b,c){控制台日志(a,b,c);}});},注销:函数(){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 的情况下获取刷新令牌,将其与原始 access_token 一起存储以备后用,并且 B) 还存储一个 client_id?有人告诉我这是一个安全问题,但我以后如何使用这些,但在仅 JS 的应用程序中保护它们?再次查看上面的代码以了解到目前为止我是如何实现的.

解决方案

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

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

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

  1. cookie 或 localstorage 都会为您提供跨页面刷新的本地持久性.将访问令牌存储在本地存储中可以为您提供更多的 CSRF 攻击保护,因为它不会像 cookie 那样自动发送到服务器.您的客户端 javascript 需要将其从 localstorage 中拉出并在每个请求中传输它.我正在开发一个 OAuth 2 应用程序,因为它是单页方法,所以我两者都不做;相反,我只是将其保存在内存中.
  2. 我同意……如果您将 cookie 存储在 cookie 中,它只是为了持久性而不是为了过期,当令牌过期时,服务器将响应错误.我认为您可能会创建一个过期的 cookie 的唯一原因是,您可以检测它是否已过期,而无需首先发出请求并等待错误响应.当然,您可以通过保存已知的到期时间对本地存储做同样的事情.
  3. 我相信这是整个问题的关键......我如何在没有 A 的情况下获得刷新令牌,将其与原始 access_token 一起存储以备后用,并且 B) 还存储一个 client_id".不幸的是,您真的不能... 正如该介绍性评论中所述,拥有刷新令牌 客户端会否定访问令牌 有限生命周期提供的安全性.我在我的应用程序中所做的事情(我没有使用任何持久的服务器端会话状态)如下:

  • 用户向服务器提交用户名和密码
  • 服务器然后将用户名和密码转发到 OAuth 端点,在上面的示例中http://domain.com/api/oauth/token,并接收访问令牌和刷新令牌.
  • 服务器加密刷新令牌并将其设置在 cookie 中(应该是 HTTP Only)
  • 服务器使用仅访问令牌以明文(在 JSON 响应中)和加密的仅 HTTP cookie 进行响应
  • 客户端javascript现在可以读取和使用访问令牌(存储在本地存储或其他地方
  • 当访问令牌过期时,客户端向服务器(不是 OAuth 服务器,而是托管应用程序的服务器)提交一个新令牌请求
  • 服务器接收它创建的加密的 HTTP only cookie,对其进行解密以获取刷新令牌,请求一个新的访问令牌,最后返回新的访问令牌响应.

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

关于 CSRF 的注意事项:如评论中所述,此解决方案未解决 跨站请求伪造;请参阅 OWASP CSRF 预防备忘单,了解有关解决的更多想法这些形式的攻击.<​​/p>

另一种选择是根本不请求刷新令牌(不确定这是否是您正在处理的 OAuth 2 实现的一个选项;刷新令牌是可选的 根据规范),并在到期时不断重新验证.

希望有帮助!

I am currently experimenting with OAuth2 to develop a mobile application built entirely in JavaScript that talks to a CakePHP API. Take a look at the following code to see how my app currently looks (please note that this is an experiment, hence the messy code, and lack of structure in areas, etc..)

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

I have a number of questions relating to my implementation of OAuth:

1.) Apparently storing the access_token in localStorage is bad practice and I should instead be using cookies. Can anyone explain why? As this isn't anymore secure or less secure as far as I can tell, as the cookie data wouldn't be encrypted.

UPDATE: According to this question: Local Storage vs Cookies storing the data in localStorage is ONLY available on the client-side anyways and doesn't do any HTTP request unlike cookies, so seems more secure to me, or least doesn't seem to have any issues as far as I can tell!

2.) Relating to question 1, use of a cookie for expiration time, would equally be pointless to me, as if you look at the code, a request is made on app start to get the user info, which would return an error if it had expired on the server end, and require a refresh_token. So not sure of benefits of having expiry times on BOTH client and server, when the server one is what really matters.

3.) How do I get a refresh token, without A, storing it with the original access_token to use later, and B) also storing a client_id? I've been told this is a security issue, but how can I use these later, but protect them in a JS-only app? Again see the code above to see how I have implemented this so far.

解决方案

It looks like you're using the Resource Owner Password Credentials OAuth 2.0 flow e.g. submitting username/pass to get back both an access token and refresh token.

  • The access token CAN be exposed in javascript, the risks of the access token being exposed somehow are mitigated by its short lifetime.
  • The refresh token SHOULD NOT be exposed to client-side javascript. It's used to get more access tokens (as you're doing above) but if an attacker was able to get the refresh token they'd be able to get more access tokens at will until such time as the OAuth server revoked the authorization of the client for which the refresh token was issued.

With that background in mind, let me address your questions:

  1. Either a cookie or localstorage will give you local persistence across page refreshes. Storing the access token in local storage gives you a little more protection against CSRF attacks as it will not be automatically sent to the server like a cookie will. Your client-side javascript will need to pull it out of localstorage and transmit it on each request. I'm working on an OAuth 2 app and because it's a single page approach I do neither; instead I just keep it in memory.
  2. I agree... if you're storing in a cookie it's just for the persistence not for expiration, the server is going to respond with an error when the token expires. The only reason I can think you might create a cookie with an expiration is so that you can detect whether it has expired WITHOUT first making a request and waiting for an error response. Of course you could do the same thing with local storage by saving that known expiration time.
  3. This is the crux of the whole question I believe... "How do I get a refresh token, without A, storing it with the original access_token to use later, and B) also storing a client_id". Unfortunately you really can't... As noted in that introductory comment, having the refresh token client side negates the security provided by the access token's limited lifespan. What I'm doing in my app (where I'm not using any persistent server-side session state) is the following:

  • The user submits username and password to the server
  • The server then forwards the username and password to the OAuth endpoint, in your example above http://domain.com/api/oauth/token, and receives both the access token and refresh token.
  • The server encrypts the refresh token and sets it in a cookie (should be HTTP Only)
  • The server responds with the access token ONLY in clear text (in a JSON response) AND the encrypted HTTP only cookie
  • client-side javascript can now read and use the access token (store in local storage or whatever
  • When the access token expires, the client submits a request to the server (not the OAuth server but the server hosting the app) for a new token
  • The server, receives the encrypted HTTP only cookie it created, decrypts it to get the refresh token, requests a new access token and finally returns the new access token in the response.

Admittedly, this does violate the "JS-Only" constraint you were looking for. However, a) again you really should NOT have a refresh token in javascript and b) it requires pretty minimal server-side logic at login/logout and no persistent server-side storage.

Note on CSRF: As noted in the comments, this solution doesn't address Cross-site Request Forgery; see the OWASP CSRF Prevention Cheat Sheet for further ideas on addressing these forms of attacks.

Another alternative is simply to not request the refresh token at all (not sure if that's an option with the OAuth 2 implementation you're dealing with; the refresh token is optional per the spec) and continually re-authenticate when it expires.

Hope that helps!

这篇关于在 HTML5 Web App 中使用 OAuth2的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆