web-dev-qa-db-fra.com

Utilisation d'OAuth2 dans HTML5 Web App

J'expérimente actuellement avec OAuth2 pour développer une application mobile entièrement construite en JavaScript qui parle à une API CakePHP. Jetez un oeil au code suivant pour voir à quoi ressemble actuellement mon application (veuillez noter qu'il s'agit d'une expérience, d'où le code désordonné et le manque de structure dans les zones, 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();
    }
};

J'ai un certain nombre de questions concernant ma mise en œuvre d'OAuth:

1.) Stocker apparemment le access_token dans localStorage est une mauvaise pratique et je devrais plutôt utiliser des cookies. Quelqu'un peut-il expliquer pourquoi? Comme cela n'est plus sécurisé ou moins sécurisé pour autant que je sache, car les données des cookies ne seraient pas cryptées.

MISE À JOUR: Selon cette question: Local Storage vs Cookies le stockage des données dans localStorage est de toute façon UNIQUEMENT disponible côté client et ne fait aucune requête HTTP contrairement à les cookies, me semble donc plus sécurisé, ou du moins ne semble pas avoir de problème pour autant que je sache!

2.) En ce qui concerne la question 1, l'utilisation d'un cookie pour le temps d'expiration, serait également inutile pour moi, comme si vous regardez le code, une demande est faite au démarrage de l'application pour obtenir les informations utilisateur, ce qui retournerait une erreur si il avait expiré du côté serveur et nécessitait un refresh_token. Donc, je ne suis pas sûr des avantages d'avoir des délais d'expiration sur le client et le serveur, lorsque celui du serveur est ce qui compte vraiment.

3.) Comment puis-je obtenir un jeton d'actualisation, sans A, en le stockant avec le access_token d'origine pour l'utiliser plus tard, et B) en stockant également un client_id? On m'a dit qu'il s'agissait d'un problème de sécurité, mais comment puis-je les utiliser plus tard, mais les protéger dans une application JS uniquement? Voir à nouveau le code ci-dessus pour voir comment j'ai implémenté cela jusqu'à présent.

45
Cameron

Il semble que vous utilisiez le flux Informations d'identification du mot de passe du propriétaire de la ressource OAuth 2.0, par exemple soumettre un nom d'utilisateur/pass pour récupérer à la fois un jeton d'accès et un jeton d'actualisation.

  • Le jeton d'accès PEUT être exposé en javascript, les risques que le jeton d'accès soit exposé d'une manière ou d'une autre sont atténués par sa courte durée de vie.
  • Le rafraîchissement du jeton NE DEVRAIT PAS être exposé au javascript côté client. Il est utilisé pour obtenir plus de jetons d'accès (comme vous le faites ci-dessus) mais si un attaquant était en mesure d'obtenir le jeton d'actualisation, il pourrait obtenir plus de jetons d'accès à volonté jusqu'à ce que le serveur OAuth soit révoqué. l'autorisation du client pour lequel le jeton d'actualisation a été émis.

Dans ce contexte, permettez-moi de répondre à vos questions:

  1. Un cookie ou un stockage local vous donnera une persistance locale à travers les actualisations de page. Le stockage du jeton d'accès dans le stockage local vous offre un peu plus de protection contre les attaques CSRF car il ne sera pas automatiquement envoyé au serveur comme un cookie le fera. Votre javascript côté client devra le retirer du stockage local et le transmettre à chaque demande. Je travaille sur une application OAuth 2 et parce que c'est une approche d'une seule page, je ne fais ni l'un ni l'autre; à la place, je le garde en mémoire.
  2. Je suis d'accord ... si vous stockez dans un cookie, c'est juste pour la persistance et non pour l'expiration, le serveur va répondre avec une erreur lorsque le jeton expirera. La seule raison pour laquelle je peux penser que vous pourriez créer un cookie avec une expiration est pour que vous puissiez détecter s'il a expiré SANS d'abord faire une demande et attendre une réponse d'erreur. Bien sûr, vous pouvez faire la même chose avec le stockage local en enregistrant ce délai d'expiration connu.
  3. C'est le nœud de toute la question, je crois ... "Comment puis-je obtenir un jeton d'actualisation, sans A, en le stockant avec le access_token d'origine à utiliser plus tard, et B) en stockant également un client_id". Malheureusement, vous ne pouvez vraiment pas ... Comme indiqué dans ce commentaire introductif, avoir le côté client refresh token annule la sécurité fournie par la durée de vie limitée de token d'accès. Ce que je fais dans mon application (où je n'utilise aucun état de session côté serveur persistant) est le suivant:
    • L'utilisateur soumet son nom d'utilisateur et son mot de passe au serveur
    • Le serveur transmet ensuite le nom d'utilisateur et le mot de passe au point de terminaison OAuth, dans votre exemple ci-dessus http://domain.com/api/oauth/token, Et reçoit à la fois le jeton d'accès et le jeton d'actualisation =.
    • Le serveur crypte le rafraîchir le jeton et le place dans un cookie (devrait être HTTP uniquement)
    • Le serveur répond avec le jeton d'accès UNIQUEMENT en texte clair (dans une réponse JSON) ET le cookie HTTP chiffré uniquement
    • javascript côté client peut désormais lire et utiliser le jeton d'accès (stocker dans le stockage local ou autre
    • Lorsque le jeton d'accès expire, le client soumet une demande au serveur (pas au serveur OAuth mais au serveur hébergeant l'application) pour un nouveau jeton
    • Le serveur, reçoit le cookie HTTP chiffré uniquement qu'il a créé, le déchiffre pour obtenir le jeton d'actualisation, demande un nouveau jeton d'accès et renvoie finalement le nouveau jeton d'accès dans la réponse.

Certes, cela viole la contrainte "JS-Only" que vous recherchiez. Cependant, a) encore une fois, vous ne devriez PAS avoir de jeton d'actualisation en javascript et b) cela nécessite une logique côté serveur assez faible à la connexion/déconnexion et aucun stockage persistant côté serveur.

Note sur CSRF: Comme indiqué dans les commentaires, cette solution ne traite pas Cross-site Request Forgery ; voir le OWASP CSRF Prevention Cheat Sheet pour plus d'idées sur la lutte contre ces formes d'attaques.

Une autre alternative consiste simplement à ne pas demander du tout le jeton d'actualisation (je ne sais pas si c'est une option avec l'implémentation OAuth 2 dont vous avez besoin; le jeton d'actualisation est facultatif selon les spécifications ) et se ré-authentifier continuellement à son expiration.

J'espère que cela pourra aider!

81
jandersen

La seule façon d'être totalement sécurisé est de ne pas stocker les jetons d'accès côté client. Toute personne ayant un accès (physique) à votre navigateur peut obtenir votre jeton.

1) Votre évaluation selon laquelle ni l'une ni l'autre n'est une excellente solution est exacte.

2) L'utilisation des délais d'expiration serait la meilleure solution si vous êtes limité au développement côté client uniquement. Cela n'obligerait pas vos utilisateurs à se ré-authentifier avec Oauth aussi souvent, et garantirait que le jeton ne vivrait pas éternellement. Toujours pas le plus sûr.

3) L'obtention d'un nouveau jeton nécessiterait d'effectuer le flux de travail Oauth pour obtenir un nouveau jeton. L'identifiant client_id est lié à un domaine spécifique pour que Oauth fonctionne).

La méthode la plus sûre pour conserver les jetons Oauth serait une implémentation côté serveur.

3
rsnickell

Pour une approche purement côté client uniquement, si vous en avez l'occasion, essayez d'utiliser "Implicit Flow" plutôt que "Resource owner flow". Vous ne recevez pas de jeton d'actualisation dans le cadre de la réponse.

  1. Lorsque la page d'accès utilisateur JavaScript vérifie pour access_token dans localStorage et vérifie expires_in
  2. Si elle est manquante ou expirée, l'application ouvre un nouvel onglet et redirige l'utilisateur vers la page de connexion, après une connexion réussie, l'utilisateur est redirigé avec un jeton d'accès qui est géré côté client uniquement et conservé dans le stockage local avec la page de redirection
  3. La page principale peut avoir un mécanisme d'interrogation sur le jeton d'accès dans le stockage local et dès que l'utilisateur s'est connecté (la page de redirection enregistre le jeton dans le stockage), processus de page normalement.

Dans l'approche ci-dessus, le jeton d'accès devrait durer longtemps (par exemple 1 an). S'il y a un problème avec un jeton à longue durée de vie, vous pouvez utiliser l'astuce suivante.

  1. Lorsque la page d'accès utilisateur JavaScript vérifie pour access_token dans localStorage et vérifie expires_in
  2. Si elle est manquante ou expirée, l'application ouvre l'iframe cachée et tente de se connecter à l'utilisateur. Habituellement, le site Web d'authentification a un cookie utilisateur et stocke les octrois sur le site Web du client, donc la connexion se produit automatiquement et le script dans iframe remplira le jeton dans le stockage
  3. La page principale du client définit le mécanisme d'interrogation sur access_token et timeout. Si pendant cette courte période, le access_token n'est pas rempli dans le stockage, cela signifie que nous devons ouvrir un nouvel onglet et mettre le flux implicite normal en mouvement
1
Nick Petrus