web-dev-qa-db-ja.com

HTML5 WebアプリでOAuth2を使用する

現在、CakePHP APIと通信するJavaScriptのみで構築されたモバイルアプリケーションを開発するために、OAuth2を実験しています。次のコードを見て、私のアプリの現在の状態を確認してください(これは実験であり、コードが乱雑で、エリアの構造が欠けているなどのことに注意してください)。

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.)localStorageにaccess_tokenを保存するのは悪い習慣であり、代わりにCookieを使用する必要があります。誰でもその理由を説明できますか?これは、Cookieデータが暗号化されないため、私が知る限り、これ以上安全ではないか、安全性が低いためです。

更新:この質問によると、 Local Storage vs Cookies localStorageにデータを保存することは、クライアント側でのみ利用可能であり、Cookieとは異なり、HTTP要求を行わないため、私にとってより安全であるか、少なくとも私が知る限り問題はないようです!

2.)質問1に関連して、有効期限にCookieを使用することは、コードを見ると、アプリの起動時にユーザー情報を取得する要求が行われるため、同様に無意味です。サーバー側で有効期限が切れており、refresh_tokenが必要です。そのため、クライアントとサーバーの両方で有効期限を設定することの利点は、サーバーが本当に重要な場合にわかりません。

3.)更新トークンを取得するには、Aなしで、後で使用するために元のaccess_tokenで保存し、B)client_idも保存しますか?これはセキュリティの問題であると言われましたが、これらを後で使用する方法はありますが、JS専用アプリで保護できますか?もう一度上記のコードを参照して、これまでどのようにこれを実装したかを確認してください。

45
Cameron

Resource Owner Password Credentials OAuth 2.0フローを使用しているようです。たとえば、ユーザー名/パスを送信してアクセストークンと更新トークンの両方を取得します。

  • アクセストークンはJavaScriptで公開できますが、アクセストークンが公開されるリスクは、その短い存続期間によって何らかの形で軽減されます。
  • refresh tokenは、クライアント側のJavaScriptに公開されるべきではありません。 (上記で行っているように)より多くのアクセストークンを取得するために使用されますが、攻撃者が更新トークンを取得できた場合、OAuthサーバーは、refreshトークンが発行されたクライアントの許可を取り消しました。

その背景を念頭に置いて、あなたの質問に答えさせてください:

  1. Cookieまたはlocalstorageのいずれかを使用すると、ページの更新全体でローカルの永続性が得られます。アクセストークンをローカルストレージに保存すると、Cookieのようにサーバーに自動的に送信されないため、CSRF攻撃に対する保護が少し強化されます。クライアント側のjavascriptは、localstorageからそれを引き出して、リクエストごとに送信する必要があります。私はOAuth 2アプリで作業していますが、単一ページのアプローチであるため、どちらも行いません;代わりに、メモリに保持します。
  2. 私は同意します... Cookieに保存している場合、有効期限ではなく永続性のためだけであり、トークンの有効期限が切れるとサーバーはエラーで応答します。有効期限付きのCookieを作成できると思う唯一の理由は、最初に要求を作成してエラー応答を待つことなく、有効期限が切れているかどうかを検出できるようにするためです。もちろん、既知の有効期限を保存することで、ローカルストレージで同じことを行うことができます。
  3. これは、私が信じる質問全体の核心です...「Aなしでリフレッシュトークンを取得し、後で使用するために元のaccess_tokenで保存し、B)client_idも保存する方法」。残念ながら、あなたは本当にできません...導入コメントで述べたように、refresh tokenクライアント側を持つと、アクセストークンの限られた寿命。私のアプリで行っていること(サーバー側の永続的なセッション状態を使用していない場合)は次のとおりです。
    • ユーザーはサーバーにユーザー名とパスワードを送信します
    • serverはユーザー名とパスワードをOAuthエンドポイント、上記の例ではhttp://domain.com/api/oauth/token、およびアクセストークンとリフレッシュトークンの両方を受け取ります。
    • サーバーはrefreshトークンを暗号化し、Cookieに設定します(HTTPのみである必要があります)
    • サーバーは、アクセストークンONLY(JSON応答内)および暗号化されたHTTPのみのCookieで応答します
    • クライアント側のjavascriptは、アクセストークン(ローカルストレージなどに保存)を読み取って使用できるようになりました
    • アクセストークンの有効期限が切れると、クライアントは新しいトークンのリクエストをサーバーに送信します(OAuthサーバーではなく、アプリをホストするサーバー)。
    • サーバーは、作成した暗号化されたHTTPのみのCookieを受け取り、復号化してrefreshトークンを取得し、新しいアクセストークンを要求し、最後に新しい応答内のアクセストークン

確かに、これは探していた「JSのみ」の制約に違反します。ただし、a)JavaScriptでリフレッシュトークンを実際に使用しないでください。b)ログイン/ログアウト時にサーバー側のロジックが最小限で済み、サーバー側の永続的なストレージは必要ありません。

CSRFに関する注意:コメントで述べたように、この解決策は対処しません クロスサイトリクエストフォージェリ ;これらの形式の攻撃に対処するためのさらなるアイデアについては、 OWASP CSRF防止チートシート を参照してください。

別の代替方法は、単にリフレッシュトークンをまったく要求しないことです(それがOAuth 2実装のオプションであるかどうか不明です。リフレッシュトークンはオプションです spec )有効期限が切れたときに継続的に再認証します。

お役に立てば幸いです!

81
jandersen

完全に安全にする唯一の方法は、クライアント側のアクセストークンを保存しないことです。ブラウザに(物理的に)アクセスできる人は誰でもトークンを取得できます。

1)どちらも優れたソリューションではないというあなたの評価は正確です。

2)有効期限の使用は、クライアント側の開発のみに制限されている場合に最適です。ユーザーがOauthのように頻繁に再認証する必要はなく、トークンが永久に存続しないことを保証します。それでも、最も安全ではありません。

3)新しいトークンを取得するには、Oauthワークフローを実行して新しいトークンを取得する必要があります。client_idは、特定のドメインに関連付けられてOauthが機能します。

Oauthトークンを保持するための最も安全な方法は、サーバー側の実装です。

3
rsnickell

純粋なクライアント側のみのアプローチの場合、機会があれば、「リソース所有者のフロー」ではなく "Implicit Flow" を使用してみてください。応答の一部として更新トークンを受け取りません。

  1. ユーザーアクセスページのJavaScriptがlocalStorageのaccess_tokenをチェックし、expires_inをチェックするとき
  2. 見つからないか期限切れの場合、アプリケーションは新しいタブを開き、ログインページにユーザーをリダイレクトします。成功したログインユーザーは、クライアント側のみで処理され、リダイレクトページでローカルストレージに保存されるアクセストークンでリダイレクトされます
  3. メインページにはローカルストレージのアクセストークンに対するポーリングメカニズムがあり、ユーザーがログイン(リダイレクトページがトークンをストレージに保存)するとすぐにページ処理が正常に行われる場合があります。

上記のアプローチでは、アクセストークンは長生きする必要があります(1年など)。長寿命トークンに懸念がある場合は、次のトリックを使用できます。

  1. ユーザーアクセスページのJavaScriptがlocalStorageのaccess_tokenをチェックし、expires_inをチェックするとき
  2. 見つからないか期限切れの場合、アプリケーションは非表示のiframeを開き、ユーザーのログインを試みます。通常、認証WebサイトにはユーザーCookieがあり、クライアントWebサイトへの許可が保存されるため、ログインは自動的に行われ、iframe内のスクリプトはトークンをストレージに格納します
  3. クライアントのメインページは、access_tokenとタイムアウトにポーリングメカニズムを設定します。この短い期間中にaccess_tokenがストレージに読み込まれない場合、新しいタブを開いて通常の暗黙的なフローを設定する必要があることを意味します
1
Nick Petrus