Improving the Secured File Download UX for Unauthenticated Users
DotNetNuke has extensive security features. One of which is enforcing role based permissions when accessing files. The general workflow is to say that "Registered Users" get to see a particular file, you then create a link to that file using DNN, and as a matter of course, the roles are enforced. This uses what's known as a file ticket to request the file, so that it is not linked to directly.
This is great if you are only providing those links to currently authenticated users. However, if you want to provide these links to unauthenticated users, you get a pretty unfriendly experience. Granted, this is an edge use-case, and an argument could be made against doing this in the first place, but every project is different, and this just might make sense for you.
Background
There are a couple of ways to get yourself into this situation - the one I set up was the following:
- Drop a links module on the page,
- add a new link to a file
- Go to the file manager, and make that file's folder available only to registered users.
- Make sure the module is visible to all users
Currently, in DNN 4.9, you get a blank white page (even if you view source) with the following content:
You do not have permission to view this file.
Getting better
We could easily make that better by localizing it with a link to the login page. This improves the user experience a little bit. But really, we can put anything we want in here. Including a full HTML page with web site branding to give the user a more pleasant experience.
The value to localize can be found in GlobalResources\SharedResources.resx under the "FilePermission.Error" key. Note that any time you make a change to this file, the app restarts. So try to be patient when making changes.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>My Temporary Page</title> </head> <body> <p>Dear user, please <a href="http://localhost/dotnetnuke_2/Home/tabid/36/ctl/Login/Default.aspx">Login</a> to view the file you have requested. Thanks, Admin.</p> </body> </html>
Actually useful?
To take it a step further, we could add a timed redirect to the page, using a meta refresh. This will give the user the ability to see a custom informational page, as well as automatically be redirected.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>My Temporary Page</title> <meta http-equiv="REFRESH" content="10;url=http://localhost/dotnetnuke_2/Home/tabid/36/ctl/Login/Default.aspx" /> </head> <body> <p>Dear user, please <a href="http://localhost/dotnetnuke_2/Home/tabid/36/ctl/Login/Default.aspx">Login</a> to view the file you have requested. You will be redirected in 10 seconds. Thanks, Admin.</p> </body> </html>
Probably a lot better now...
Now, if we wanted to get really fancy, we could ditch the meta refresh, and enhance our HTML page to use JavaScript to redirect. The great benefit of this is that we can tack on a returnurl parameter to our redirect with the file ticket's URL. This will tap into the core login behavior and deliver the appropriate content after the user has logged in.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>My Temporary Page</title> <script type="text/javascript"> <!-- function redirectToLogin() { var l = document.location.href; if (l.toLowerCase().indexOf("fileticket") != -1) { var virtualDirectory = ""; var loginPage = "/Default.aspx?ctl=login&returnurl="; var returnUrl = encodeURIComponent(l.substr(l.lastIndexOf("/"))) window.location = "/" + virtualDirectory + loginPage + encodeURIComponent("/") + virtualDirectory + returnUrl; } } --> </script> </head> <body onload="setTimeout('redirectToLogin()', 5000)"> <p>Header with site branding, navigation</p> <p><a href="javascript:redirectToLogin();">Dear user, go to the login page!</a></p> <p>Footer with site branding, navigation?</p> </body> </html>
There are a few things that you may want to modify above for your implementation:
- timeout (on body onload)- this is how fast your page will redirect to the login page/control. If you don't want a "landing page", set this to 0.
- virtualDirectory - if you are using a virtual directory (e.g. http://localhost/dotnetnuke_2/), you'll want to set this to "dotnetnuke_2"
- loginPage - relative link to the login page/control
Summary
When we want to provide a link for unauthenticated users to a file and enforce role security, we now have the ability to:
- provide that user with more information when they click on the link
- implement consistent branding on that informational page
- automatically redirect them to the login screen
- supply a return URL so that after they login, they are taken directly to the file.
As this is just a quick and dirty implementation, I'd love to see someone with a need for this actually make it awesome. (yes, that's right...I don't even have a project to implement this on...)
And as always, if you have a project to share with the community, you should submit it to dnnGallery!
A note about unwanted redirects
One catch with the meta refresh is that the DNN language editor shows you a preview of the content. So after you localize it with a meta refresh the first time, ever subsequent time you visit the SharedResources language file in the editor, the page will redirect.
I don't think the same is true of the JavaScript, but because different browsers could handle this differently, I went ahead and included a check so that the JavaScript redirect will only occur if the URL contains "FileTicket". Feel free to experiment.