Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
228 views
in Technique[技术] by (71.8m points)

javascript - Using OAuth2 in HTML5 Web App

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.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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!


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...