javascript 在 HTML5 Web App 中使用 OAuth2

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/18280827/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-10-27 11:20:10  来源:igfitidea点击:

Using OAuth2 in HTML5 Web App

javascriptoauth-2.0

提问by Cameron

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

我目前正在尝试使用 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();
    }
};

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

我有一些与我的 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.

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

UPDATE: According to this question: Local Storage vs Cookiesstoring 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!

更新:根据这个问题:将数据存储在 localStorage 中的Local Storage vs Cookies无论如何只能在客户端使用,并且不像 cookie 那样做任何 HTTP 请求,所以对我来说似乎更安全,或者至少似乎没有据我所知有任何问题!

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.

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

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.

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

回答by jandersen

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

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

  • The access tokenCAN be exposed in javascript, the risks of the access token being exposed somehow are mitigated by its short lifetime.
  • The refresh tokenSHOULD 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 tokenwas issued.
  • 令牌访问在JavaScript被曝光,令牌被莫名其妙地暴露访问的风险是由它的寿命短缓解。
  • 令牌刷新不应暴露于客户端的JavaScript。它用于获取更多访问令牌(如您在上面所做的那样)但如果攻击者能够获得刷新令牌,他们将能够随意获得更多访问令牌,直到 OAuth 服务器撤销授权为其颁发刷新令牌的客户端。

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 tokenclient 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 serverthen 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 tokenand sets it in a cookie (should be HTTP Only)
    • The server responds with the access token ONLYin 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 tokenin the response.
  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 Only)
    • 服务器仅以明文(在 JSON 响应中)和加密的 HTTP cookie使用访问令牌进行响应
    • 客户端 javascript 现在可以读取和使用访问令牌(存储在本地存储或其他任何地方)
    • 当访问令牌过期时,客户端向服务器(不是 OAuth 服务器,而是托管应用程序的服务器)提交一个新令牌请求
    • 服务器接收它创建的加密的 HTTP only cookie,对其进行解密以获取刷新令牌,请求新的访问令牌,最后在响应中返回新的访问令牌

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.

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

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

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

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.

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

Hope that helps!

希望有帮助!

回答by rsnickell

The only way to be fully secure is to not store the access tokens client side. Anyone with (physical)access to your browser could obtain your token.

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

1) Your assessment of neither being a great solution is accurate.

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

2) Using expiration times would be your best if you are limited to only client side development. It wouldn't require your users to re-authenticate with Oauth as frequently, and guarantee that the token wouldn't live forever. Still not the most secure.

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

3) Getting a new token would require performing the Oauth workflow to obtain a fresh token. The client_id is tied to a specific domain for Oauth to function.

3) 获取新令牌需要执行 Oauth 工作流以获取新令牌。client_id 与 Oauth 功能的特定域相关联。

The most secure method for retaining Oauth tokens would be a server side implementation.

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

回答by Nick Petrus

For pure client side only approach, if you have a chance, try to use "Implicit Flow"rather then "Resource owner flow". You do not receive refresh token as a part of the response.

对于纯客户端方法,如果有机会,请尝试使用“隐式流”而不是“资源所有者流”。您不会收到作为响应一部分的刷新令牌。

  1. When user access page JavaScript checks for access_token in localStorage and checks it expires_in
  2. If missing or expired then application opens new tab and redirects user to the login page, after successful login user is redirected back with access token which is handled client side only and preserved in local storage with redirect page
  3. The main page might have polling mechanism on the access token in local storage and as soon the user logged in (redirect page saves token to storage) page process normally.
  1. 当用户访问页面 JavaScript 在 localStorage 中检查 access_token 并检查它 expires_in
  2. 如果丢失或过期,则应用程序将打开新选项卡并将用户重定向到登录页面,成功登录后,用户将使用访问令牌重定向回来,该令牌仅在客户端处理并使用重定向页面保存在本地存储中
  3. 主页可能对本地存储中的访问令牌具有轮询机制,并且一旦用户登录(重定向页面将令牌保存到存储)页面处理正常。

In the above approach the access token should be long living (e.g. 1 year). If there is a concern with long living token you can use following trick.

在上述方法中,访问令牌应该是长寿的(例如 1 年)。如果您担心长寿令牌,您可以使用以下技巧。

  1. When user access page JavaScript checks for access_token in localStorage and checks it expires_in
  2. If missing or expired then application opens hidden iframe and tries to login user. Usually auth website has a user cookie and stores grant to the client website, therefore login happens automatically and script inside iframe will populate token into storage
  3. The client's main page sets polling mechanism on access_token and timeout. If during this short period the access_token is not populated into storage, it means that we need to open new tab and set normal Implicit flow in motion
  1. 当用户访问页面 JavaScript 在 localStorage 中检查 access_token 并检查它 expires_in
  2. 如果丢失或过期,则应用程序打开隐藏的 iframe 并尝试登录用户。通常 auth 网站有一个用户 cookie 并将授权存储到客户端网站,因此登录会自动发生,iframe 内的脚本会将令牌填充到存储中
  3. 客户端的主页设置了 access_token 和 timeout 的轮询机制。如果在这短短的时间内 access_token 没有填充到存储中,则意味着我们需要打开新选项卡并设置正常的隐式流