Articles
Sécuriser AngularJS avec des revendications
14 février 2015 • 6 min de lecture
À un moment donné, une application a besoin d’autorisation. Cela signifie que différents niveaux d’accès se comportent différemment sur un site Web (ou n’importe quoi d’ailleurs). Cela peut aller de la visualisation de données à des zones entières qui ne sont pas accessibles à un groupe d’utilisateurs.
Dans les applications non Single Page (SPA), une revendication ou un rôle est associé à des données ou à une zone de l’application, soit l’utilisateur a ce rôle ou cette revendication, soit il ne l’a pas. Dans une SPA c’est la même chose, mais avec une énorme mise en garde. Une SPA est téléchargée dans le navigateur. À ce stade, le navigateur a un contrôle total sur le code. Une personne malveillante peut modifier le code pour servir ses intérêts.
Parce que les SPA ne peuvent pas être sécurisées, l’authentification et l’autorisation dans une SPA sont simplement une expérience utilisateur. Toute sécurité significative doit être effectuée sur le serveur Web. Cet article ne couvre pas la sécurisation de votre API contre les attaques. Je recommande de regarder une vidéo de Pluralsight ou de lire un document qui aborde la sécurité pour votre technologie serveur.
L’intention de cet article est de vous montrer comment j’ai ajouté une expérience utilisateur d’autorisation à mon SPA Angular 1.x.
Portées de sécurité
J’ai identifié 3 zones de l’interface utilisateur qui nécessitent une autorisation : Éléments (HTML), Routes et Données.
Juste un rappel, sécuriser une SPA n’est pas un substitut à la sécurisation du serveur. Les permissions sur le client servent simplement à garder les honnêtes gens honnêtes et à offrir à l’utilisateur une bonne expérience.
Les 3 zones en détail :
Éléments
Vous devrez masquer des éléments HTML spécifiques. Cela pourrait être un libellé, un tableau avec des données, un bouton ou n’importe quel élément de la page.
Routes
Vous voudrez masquer des routes entières. Dans certains cas, vous ne voulez pas que l’utilisateur accède à une vue. En sécurisant la route, un utilisateur ne peut pas naviguer vers la vue. À la place, un message « Vous n’êtes pas autorisé à naviguer vers cette vue » s’affichera.
Données
Parfois, masquer les éléments dans la vue ne suffit pas. Un utilisateur avisé peut simplement consulter la source et voir les données masquées dans la source HTML ou les regarder diffuser vers le navigateur. Ce que nous voulons, c’est que les données ne soient pas récupérées en premier lieu.
Ajouter la sécurité est délicat. Au début, j’ai essayé de contraindre l’accès au niveau de l’API HTTP (sur le client). J’ai rapidement réalisé que cela ne fonctionnerait pas. Un utilisateur peut ne pas avoir un accès direct aux données, mais cela ne signifie pas qu’il n’a pas un accès indirect aux données. Au niveau de la couche API HTTP (généralement l’une des plus basses de l’application), nous ne pouvons pas dire le contexte de l’appel et donc ne pouvons pas appliquer les préoccupations de sécurité à celui-ci.
Ci-dessous, j’ai fourni des exemples de code :
Code
J’ai créé un service pour le code de vérification d’autorisation. C’est le cœur de l’autorisation. Toutes les demandes d’autorisation utilisent ce service pour vérifier si l’utilisateur est autorisé pour l’action particulière.
angular.module('services')
.service('AuthorizationContext',function(_, Session){
this.authorizedExecution = function(key, action){
//Looking for the claim key that was passed in. If it exists in the claim set, then execute the action.
Session.claims(function(claims){
var claim = findKey(key, claims);
//If Claim was found then execute the call.
//If it was not found, do nothing
if(claim !== undefined){
action();
}
});
};
this.authorized = function(key, callback){
//Looking for the claim key that was passed in. If it exists in the claim set, then execute the action.
Session.claims(function(claims){
var claim = findKey(key, claims);
//If they don't have any security key, then move forward and authorization.
var valid = claim !== undefined;
callback(valid);
});
};
//this.agencyViewKey = '401D91E7-6EA0-46B4-9A10-530E3483CE15';
function findKey(key, claims){
var claim = _.find(claims, function(item){
return item.value === key;
});
return claim;
}
});
Directive d’autorisation
La directive d’autorisation peut être appliquée à n’importe quel élément HTML que vous souhaitez masquer aux utilisateurs sans un niveau d’accès spécifique. Si l’utilisateur a le jeton d’accès dans ses revendications, il est autorisé à voir l’élément. S’il ne l’a pas, il est masqué.
angular.module('directives')
.directive('authorize', ['$compile', 'AuthorizationContext', function($compile, AuthorizationContext) {
return {
restrict: 'A',
replace: true,
//can't have isolated the scope in a shared directive
link:function ($scope, element, attributes) {
var securityKey = attributes.authorize;
AuthorizationContext.authorized(securityKey, function(authorized){
var el = angular.element(element);
el.attr('ng-show', authorized);
//remove the attribute, otherwise it creates an infinite loop.
el.removeAttr('authorize');
$compile(el)($scope);
});
}
};
}]);
Éléments
Je m’appuie fortement sur les onglets dans mon application. J’applique la directive d’autorisation à l’onglet que je veux masquer aux utilisateurs sans les revendications appropriées.
<tabset>
<tab ng-cloak heading="Users" authorize="{{allowUserManagement}}">
...html content
</tab>
</tabset>
Routes
J’utilise ui-router. Malheureusement pour ceux qui ne le font pas, je n’ai pas de code pour le routeur AngularJS prêt à l’emploi.
Dans $stateChangeStart, j’authentifie la route. Voici le code dans cet événement.
$rootScope.$on("$stateChangeStart", function(event, toState, toParams, fromState, fromParams){
AuthenticationManager.authenticate(event, toState, toParams);
});
La fonction qui autorise la route. Si elle est autorisée, la route est autorisée à continuer. Si elle n’est pas autorisée, un message s’affiche à l’utilisateur et il est dirigé vers la page d’accueil.
function authorizedRoute(toState, location, toaster, breadCrumbs){
if(toState.authorization !== undefined){
AuthorizationContext.authorized(toState.authorization, function(authorized){
if(!authorized){
toaster.pop('error', 'Error', 'You are not authorized to view this page.');
location.path("/search");
} else {
breadCrumbs();
}
});
} else{
breadCrumbs();
}
}
Dans cette définition de routeur, vous remarquerez une propriété appelée « authorization ». Si l’utilisateur a cette revendication, il est autorisé à continuer.
angular.module('agency',
[
'ui.router',
'services'
])
.config(function config($stateProvider){
$stateProvider.state( 'agency', {
url: '/agency',
controller: 'agency.index',
templateUrl: 'agency/agency.tpl.html',
authenticate: true,
authorization:'401d91e7-6ea0-46b4-9a10-530e3483ce15',
data:{ pageTitle: 'Agency' }
});
});
Données
Dans certains cas, vous ne voulez pas faire une demande au serveur pour les données. Si l’utilisateur a la revendication, il sera autorisé à faire la demande.
Le AuthorizationContext ci-dessus au début de l’article montre le code pour authoriedExecution. Voici son utilisation.
AuthorizationContext.authorizedExecution(Keys.authorization.allowUserManagement, function(){
//execute code, if the loggedin user has rights.
});
Comme je l’ai mentionné ci-dessus, ce n’est pas un substitut à la sécurisation du serveur. Ce code fonctionne pour offrir une excellente expérience utilisateur.
Auteur : Chuck Conway est un ingénieur IA avec près de 30 ans d’expérience en génie logiciel. Il construit des systèmes IA pratiques — pipelines de contenu, agents d’infrastructure et outils qui résolvent des problèmes réels — et partage ce qu’il apprend en chemin. Connectez-vous avec lui sur les réseaux sociaux : X (@chuckconway) ou visitez-le sur YouTube et sur SubStack.