Skip to Content

Hello,

I this post, I want to share a requirement about routing I have heard several times on customer Fiori project, with the solution I propose for it.

In Fiori apps, navigation between views is achieved most often with hash-based navigation, where some entity key information is added to the app url after a hash sign. That hash string is then parsed by the router according to patterns defined in the manifest.json file.

There are some good reasons to use that hash-based navigation : it allows for example to bookmark entity detail pages as own tiles in the Fiori Launchpad.

Unfortunately, the entity key information of the hash string appears clearly in the app url (unless it is a GUID), and can easily be overwritten by the user. So the requirement I hear, is to hide the entity keys in the url.

Obviously, you will tell me that the only secure means of protecting access to the data behind the entity keys is a strong authorization concept in the SAP backend. Right.

But there are situations where the Fiori project outpaces other backend projects and where it is desirable to control in the UI what users can do and what they shouldn’t.

This is the context of the solution presented: Obfuscation of the hash-string parameters.

It will not prevent someone who really wants to get the data behind ge entity keys (e.g. using Chrome’s Developer Tools) but it will provide guidance and control for the typical end user.

So let’s see how we can achieve this in a few steps:

  • We create a custom router that is a subclass of SAP’s standard router, and that handles obfuscation and deobfuscation.
  • We replace SAP’s standard router by our custom one in the routing part of the manifest.json file.

A demo app in Plunker is available here (if it does not run in IE, give it a try with Chrome).

Steps in detail:

a) Create custom router

It inherits from sap.m.routing.Router. Let’s redefine methods “navTo”, “fireBeforeRouteMatched” and “fireRouteMatched”.

In our demo app, custom router is defined within the app, but you may as well store it once into your custom code library and reuse it in different apps.

sap.ui.define([
	"sap/m/routing/Router"
], function (Router) {
	"use strict";

	return Router.extend("be.canoas.fiori.RoutingPathObfuscation.util.customRouter", {

		// redefine constructor, to initialize private attribute(s)
		constructor: function (oRoutes, oConfig, oOwner, oTargetsConfig) {

			// initialize private attribute _bRoutingPathObfuscation
			// _bRoutingPathObfuscation is only to be used in demo app (not needed in real project)
			this.setRoutingPathObfuscation(false);

			// define _obfuscate/_deobfuscate methods as properties of private attribute _oParameterHandler
			this._oParameterHandler = {};
			this._oParameterHandler._obfuscate = function (sString) {
				if (!this._bState) {
					this._bState = true;
					// base64 encoding; alternative: perform ajax call to achieve stronger encryption
					return window.btoa(sString);
				} else {
					return sString;
				}
			};
			this._oParameterHandler._deobfuscate = function (sString) {
				if (this._bState) {
					this._bState = false;
					// base64 decoding; alternative: perform ajax call to achieve stronger decryption
					return window.atob(sString);
				} else {
					return sString;
				}
			};

			// call super
			Router.prototype.constructor.call(this, oRoutes, oConfig, oOwner, oTargetsConfig);
		},

		// getter method for private attribute _bRoutingPathObfuscation
		// _bRoutingPathObfuscation is only to be used in demo app (not needed in real project)
		getRoutingPathObfuscation: function () {
			return this._bRoutingPathObfuscation;
		},
		// setter method for private attribute _bRoutingPathObfuscation
		// _bRoutingPathObfuscation is only to be used in demo app (not needed in real project)
		setRoutingPathObfuscation: function (bState) {
			this._bRoutingPathObfuscation = bState;
		},

		// method to process parameters
		_processParameters: function (oParameters, sFunctionName) {
			// _bRoutingPathObfuscation is only to be used in demo app (not needed in real project)
			if (this._bRoutingPathObfuscation) {
				for (var name in oParameters) {
					if ({}.hasOwnProperty.call(oParameters, name)) {
						oParameters[name] = this._oParameterHandler[sFunctionName](oParameters[name]);
					}
				}
			}
			return oParameters;
		},

		// redefine navTo method, for outbound part of navigation
		navTo: function (sName, oParameters, bReplace) {

			// obfuscate parameters
			var oChangedParameters = this._processParameters(oParameters, "_obfuscate");

			// call super
			Router.prototype.navTo.call(this, sName, oChangedParameters, bReplace);
		},

		// redefine fireBeforeRouteMatched method, for inbound part of navigation 
		// (called only if target needs to be loaded and placed)
		fireBeforeRouteMatched: function (oEventData) {

			// deobfuscate parameters
			var oChangedParameters = this._processParameters(oEventData.arguments, "_deobfuscate");
			oEventData.arguments = oChangedParameters;

			// call super
			Router.prototype.fireBeforeRouteMatched.call(this, oEventData);
		},

		// redefine fireRouteMatched method, for inbound part of navigation 
		// (called in any case if a route matches the URL hash)
		fireRouteMatched: function (oEventData) {

			// deobfuscate parameters
			var oChangedParameters = this._processParameters(oEventData.arguments, "_deobfuscate");
			oEventData.arguments = oChangedParameters;

			// call super
			Router.prototype.fireRouteMatched.call(this, oEventData);
		}

		// don't need to redefine fireRoutePatternMatched method, as it is expected to always be triggered
		// after either fireBeforeRouteMatched or fireRouteMatched, which means that at this point
		// deobfuscation of parameters has already occurred

		// don't need to redefine fireBypassed method, as it is not expected to be triggered 
		// after a call of navTo method, but rather after a manual change of the URL hash (without a
		// route being associated to it)

	});
});

b) Replace SAP’s standard router by custom one in manifest.json

c) ensure custom router gets instantiated in component.js.

Prototype of init method will try to get an instance of router defined in manifest.json router configuration, in order to initialize it. For this to work, we force instantiation of our custom router in the very first lines of the “define” statement.

Enhancement of a standard app

Now if you want to implement this custom router into an enhancement project of a standard app, code may look slightly different, but this is not an issue.

If component is instantiated with “declare” instead of “define”, just add custom router with a “require” statement.

Regarding manifest.json, copy complete “routing” configuration part from original app into your enhancement project’s manifest.json file under “sap.ui5”, after the declaration of the extensions, and replace router class.

 

Hope this can be useful to somebody

Walter

To report this post you need to login first.

1 Comment

You must be Logged on to comment or reply to a post.

Leave a Reply