Technical Articles
Implementing User Access Restrictions
User (or entity) authentication, session management and access authorization can never be provided on the client-side. It is beyond the scope of this text to deal with these issues. For general information about authentication you may want to read the Authentication Cheat Sheet from the Open Web Application Security Project (OWASP).
In this post, I just want to give you an idee how to implement access control to help protect a client application from unauthorized use. Although we use the user’s role to grant or deny access, this shall not be understood as a statement for role-based vs. attribute-based access control.
Here, we simply assume that the user already has a verified session and that we can get the user profile from the authorizing server. We further assume, that the response is JSON with some basic information about the user.
{
"firstname": "Carsten",
"lastname": "Heinrigs",
"userId": "cahein",
"token": "562f667540ce09f2a6461920011b341e",
"roles": [
"user"
]
}
To begin with, we have to request the user profile and set the related variables.
Note: The below code examples use functions from the OpenUI5 Library project (https://github.com/cahein/oui5lib). However, you may implement the functionality however you like.
To group the related functions, the namespace oui5lib.currentuser is being used.
To request the user profile, the oui5lib.request.fetchJson function (https://github.com/cahein/oui5lib/blob/master/webapp/request.js) is used with a callback function userProfileRequestSucceeded.
(function(request) {
"use strict";
const user = oui5lib.namespace("currentuser"),
_name = null,
_userId = null,
_token = null,
_userRoles = [],
_permissionsMap = null;
function init() {
const userProfileUrl = "https://auth.somehost.com/getUserProfile";
requestUserProfile(userProfileUrl);
}
user.init = init;
function requestUserProfile(userProfileUrl) {
oui5lib.request.fetchJson(userProfileUrl,
userProfileRequestSucceeded,
{}, false);
}
function userProfileRequestSucceeded(userProfile) {
if (userProfile !== null) {
_name = userProfile.firstname + " " + userProfile.lastname;
_userId = userProfile.userId;
_token = userProfile.token;
if (userProfile.roles instanceof Array) {
_userRoles = userProfile.roles;
}
}
}
}(oui5lib.request));
So far, the only function added to the oui5lib.currentuser namespace is the init function. In this case we definitely want further processing of our application to be blocked until the issue of authorization is settled. We therefore request the user profile synchronously since any further steps depend on the response.
If the user doesn’t have the required authorization, he or she clearly shouldn’t be able to access the application and instead be redirected to a noPermissions page. We add the related route and target to the manifest:
"routes": [
...
{
"pattern": "notAuthorized",
"name": "notAuthorized",
"target": "notAuthorized"
}
],
"targets": {
...
"notAuthorized": {
"viewName": "noPermissions",
"viewType": "XML"
}
}
As target view, we create a simple MessagePage. For productive environments, the texts below should be multilingual i18n properties.
<mvc:View xmlns="sap.m" xmlns:mvc="sap.ui.core.mvc">
<MessagePage
title="Not Authorized"
text="This page is not available."
description="You do not have permission to access the requested page."/>
</mvc:View>
Because there are many ways to navigate to views, the only reliable way to check permissions is on the view level. Following widespread practice we configure permissions based upon roles. Let us create a permissions.json and add view entries.
{
"views": {
"oum.view.entry": {
"roles": [ "user" ]
},
"oum.view.unaccessible": {
"roles": [ "noone", "can", "go", "here" ]
}
}
}
To load the permissions, we request it in the oui5lib.currentuser.init function. Again the request is synchronous for the same reason as stated above.
function init() {
...
oui5lib.request.fetchJson("permissions.json",
permissionsMapRequestSucceeded,
{}, false);
}
function permissionsMapRequestSucceeded(permissionsMap) {
_permissionsMap = permissionsMap;
}
To check if a user belongs to the required group(s), we add the following function to the oui5lib.currentuser namespace.
The hasPermissionsForView function gives no authorization if the requested ‘permissions.json’ couldn’t be loaded and processed. If, however, there is no entry in the permissions map, it is assumed that access is authorized. In case there is an entry with an empty roles array, no user will be authorized.
function hasPermissionForView(viewName) {
if (_permissionsMap === null) {
return false;
}
if (typeof _permissionsMap.views[viewName] !== "undefined") {
const roles = _permissionsMap.views[viewName].roles;
return hasPermissions(roles);
}
return true;
}
function hasPermissions(authorizedRoles) {
if (typeof authorizedRoles === "undefined" ||
!(authorizedRoles instanceof Array)) {
return false;
}
let authorized = false, i, s;
for (i = 0, s = authorizedRoles.length; i < s; i++) {
if (hasRole(authorizedRoles[i])) {
authorized = true;
break;
}
}
return authorized;
}
function hasRole(role) {
return _userRoles.indexOf(role) > -1;
}
user.hasPermissionForView = hasPermissionForView;
user.hasRole = hasRole;
To check permission to a particular view, we have to add a few lines to the connected controller onInit function.
onInit: function () {
const view = this.getView();
if (!oui5lib.currentuser.hasPermissionForView(view.getViewName())) {
this.getRouter().navTo("notAuthorized");
return;
}
...
}
Because this needs to be repeated for every view with a target, we ought to further simplify matters by adding a function to a custom base Controller, which the individual controllers extend.
verifyPermission: function() {
const view = this.getView();
const viewName = view.sViewName;
if (!oui5lib.currentuser.hasPermissionForView(viewName)) {
this.getRouter().navTo("notAuthorized");
return false;
}
return true;
}
This significantly reduces the verification code in the onInit function.
onInit: function () {
if (!this.verifyPermission()) {
return;
}
...
}
All our efforts would be in vain, however, if a malicious user could easily circumvent our safety precautions. So far, it would be enough to overwrite the hasPermissionForView function in the browser console.
oui5lib.currentuser.hasPermissionForView = function() {
return true;
};
To make it more difficult we have to freeze the involved objects, so that they cannot be modified afterwards.
function deepFreeze(o) {
let prop, propKey;
Object.freeze(o);
for (propKey in o) {
prop = o[propKey];
if (!(o.hasOwnProperty(propKey) && (typeof prop === "object")) ||
Object.isFrozen(prop)) {
continue;
}
this.deepFreeze(prop);
}
}
Now, we should have everything in place to effectively use the oui5lib.currentuser object. The best place to initialize it is the Component init function before initializing the Router.
if (typeof oui5lib.currentuser === "object") {
oui5lib.currentuser.init();
deepFreeze(oui5lib.currentuser);
deepFreeze(oui5lib.request);
}
Nothing happens unless the oui5lib.currentuser object is available. When it is available, the init function is called. It will request the user profile and also fetch the configured permissions. To prevent easy tampering, we freeze both the user object and the request code.
Maybe there is a way to hack this solution, but I have no idee how 😉