6

Before beginning this question, I should point out that my knowledge of ASP.NET & C# is pretty much nil.

I'm in the process of trying to integrate the ASP.NET version of CKFinder v3 into a site built in a different language and all is going well so far; I have everything setup as I want it and it's working when I grant unrestricted access to CKF but I'm stuck at the point now of trying to restrict access to it by authenticating only certain members of my site to use it. All the pages that CKFinder appears on on my site are only accessible by those certain members but I need an extra level of security if, for example, anyone figures out the direct path to my "ckfinder.html" file.

In the ASP version of CKFinder, I simply added this line in the function that checks my member's privileges, where isEditor was a boolean whose value was assigned per member based on information from my database:

session("accessckf")=isEditor

and then edited the CheckAuthentication() function in CKFinder's "config.asp" file to read:

function CheckAuthentication()
    CheckAuthentication=session("accessckf")
end function

Reading through this "Howto", authentication seems to be much more complex in v3 but, after a lot of trial and error and some help from Lesiman, I created this C# file, which is located in my CKF directory:

<%@page codepage="65001" debug="true" language="c#" lcid="6153"%>
<%@import namespace="CKSource.CKFinder.Connector.Core"%>
<%@import namespace="CKSource.CKFinder.Connector.Core.Authentication"%>
<%@import namespace="CKSource.CKFinder.Connector.Core.Builders"%>
<%@import namespace="CKSource.CKFinder.Connector.Host.Owin"%>
<%@import namespace="Owin"%>
<%@import namespace="System.Data.Odbc"%>
<%@import namespace="System.Threading"%>
<%@import namespace="System.Threading.Tasks"%>
<script runat="server">
    public void Configuration(IAppBuilder appBuilder){
        var connectorBuilder=ConfigureConnector();
        var connector=connectorBuilder.Build(new OwinConnectorFactory());
        appBuilder.Map("/path/to/connector",builder=>builder.UseConnector(connector));
    }
    public ConnectorBuilder ConfigureConnector(){
        var connectorBuilder=new ConnectorBuilder();
        connectorBuilder.SetAuthenticator(new MyAuthenticator());
        return connectorBuilder;
    }
    public class MyAuthenticator:IAuthenticator{
        public Task<IUser> AuthenticateAsync(ICommandRequest commandRequest,CancellationToken cancellationToken){
            var domain=HttpContext.Current.Request.Url.Host;
            var cookie=HttpContext.Current.Request.Cookies[urlDomain];
            var password="";
            var username="";
            var user=new User(false,null);
            if (cookie!=null){
                if (cookie["username"]!=null)
                    username=cookie["username"];
                if (cookie["password"]!=null)
                    password=cookie["password"];
                if(username!=""&&password!=""){
                    var connection=new OdbcConnection("database=[database];driver=MySQL;pwd=[pwd];server=[server];uid=[uid];");
                    connection.Open();
                    OdbcDataReader records=new OdbcCommand("SELECT ISEDITOR FROM MEMBERS WHERE USERNAME='"+username+"' AND PASSWORD='"+password+"'",connection).ExecuteReader();
                    if(records.HasRows){
                        records.Read();
                        bool isEditor=records.GetString(0)=="1";
                        var roles="member";
                        if(isEditor)
                            roles="editor,member";
                            user=new User(isEditor,roles.Split(','));
                        }
                        records.Close();
                        connection.Close();
                }
            }
            return Task.FromResult((IUser)user);
        }
    }
</script>

Loading that page produces no errors (which doesn't necessarily mean it's working as trying to write anything to screen from within the public class doesn't work, for some reason) so now I'm at the stage of somehow checking that file for authentication.

Originally, I had tried loading it via XMLHttp from within my function that checks membership privileges for the site but, as I suspected and as Lesmian confirmed, that wouldn't work. After more trial & error, I added code to check website member privileges to the C# file, which leads me to where I am now: stuck!

What do I need to edit within CKFinder in order to have it use this custom file to check whether or not a user is authenticated?

Community
  • 1
  • 1
Shaggy
  • 6,696
  • 2
  • 25
  • 45
  • I think if you added more details, folks familiar with ASP and ASP.NET, but NOT familiar with ckfinder might be able to help. Things I'd like to know - what pages of each are involved, what is your current authorization method and what use case/flow we're looking to have. – G. Stoynev May 24 '16 at 16:10
  • I've updated the question to clean it up and provide more detail. Let me know if you need more than that @G.Stoynev. – Shaggy May 24 '16 at 16:54
  • I'm a little bit confused. Which authentication method you want to use, Classic ASP session variable or database? – Kul-Tigin May 27 '16 at 21:18
  • Apologies for the confusion, I included the old ASP authentication method to illustrate how I'm *used* to being able to grant members access, in the hopes that there's an equally easy method in the new ASP.NET version that I'm not yet aware of. Failing that, I've come up with the C# file included in the question based on the CKF documentation and a *lot* of help from SO and am trying to figure out how to use that file to grant access to CKF. – Shaggy May 27 '16 at 22:30

2 Answers2

3

First you'll need a connector between the ASP's Session and CKFinder's .Net authenticator. Here's an example that serializes ASP Session contents into JSON.

Put the connector.asp into a publicly accessible location. http://myaspwebsite.com/connector.asp for example.

connector.asp

<%@Language=VBScript CodePage=65001%>
<% Option Explicit %>
<!--#include file="JSON.asp"-->
<%
' obtain JSON.asp from https://github.com/tugrul/aspjson/archive/master.zip

' just for testing, must be removed in the production environment
Session("isEditor") = True
Session("isMember") = True

' only local requests allowed
' instead of local and remote ip comparison, a secret key can be used
If Request.ServerVariables("LOCAL_ADDR") <> Request.ServerVariables("REMOTE_ADDR") Then
    Response.Status = "403 Forbidden"
    Response.End
End If

Response.ContentType = "application/json"
Response.Charset = "utf-8"

Dim JSONObject, Key
Set JSONObject = jsObject()

For Each Key In Session.Contents
    If Not IsObject(Session.Contents(Key)) Then 'skip the objects cannot be serialized
        JSONObject(Key) = Session.Contents(Key)
    End If
Next

JSONObject.Flush
%>

CKFinder 3.3.0 comes with a default connector which can be found in /ckfinder/bin/CKSource.CKFinder.Connector.WebApp.dll, remove it.

Examine the following program and remember to replace builder.Map("/connector", SetupConnector); and new Uri("http://myaspwebsite.com/connector.asp"); with your own values.

It simply authenticates the users by checking ASP Session varaibles isEditor and isMember via connector.asp and finally claims the roles editor , member or none.

I assume that you have configured the roles editor and member in the web.config.

Then put the Shaggy.cs into /ckfinder/App_Code. Create App_Code directory if not exist. .Net files in this folder will be compiled on the fly.

For more information have a look at Shared Code Folders in ASP.NET Web Projects

Shaggy.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Newtonsoft.Json.Linq;
using Owin;

[assembly: Microsoft.Owin.OwinStartup(typeof(CKSource.CKFinder.Connector.Shaggy.Startup))]
namespace CKSource.CKFinder.Connector.Shaggy
{
    using FileSystem.Local;
    using FileSystem.Dropbox;
    using Core;
    using Core.Authentication;
    using Config;
    using Core.Builders;
    using Core.Logs;
    using Host.Owin;
    using Logs.NLog;
    using KeyValue.EntityFramework;

    public class Startup
    {
        public void Configuration(IAppBuilder builder)
        {
            LoggerManager.LoggerAdapterFactory = new NLogLoggerAdapterFactory();

            RegisterFileSystems();

            builder.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = "ApplicationCookie",
                AuthenticationMode = AuthenticationMode.Active
            });

            //replace connector path with yours
            builder.Map("/connector", SetupConnector);
        }

        private static void RegisterFileSystems()
        {
            FileSystemFactory.RegisterFileSystem<LocalStorage>();
            FileSystemFactory.RegisterFileSystem<DropboxStorage>();
        }

        private static void SetupConnector(IAppBuilder builder)
        {
            var keyValueStoreProvider = new EntityFrameworkKeyValueStoreProvider("CacheConnectionString");
            var authenticator = new ShaggysAuthenticator();

            var connectorFactory = new OwinConnectorFactory();
            var connectorBuilder = new ConnectorBuilder();
            var connector = connectorBuilder
                .LoadConfig()
                .SetAuthenticator(authenticator)
                .SetRequestConfiguration(
                    (request, config) =>
                    {
                        config.LoadConfig();
                        config.SetKeyValueStoreProvider(keyValueStoreProvider);
                    })
                .Build(connectorFactory);

            builder.UseConnector(connector);
        }
    }

    public class ShaggysAuthenticator : IAuthenticator
    {
        // this method makes an http request on the background to gather ASP's all session contents and returns a JSON object
        // if the request contains ASP's session cookie(s)
        private static JObject GetAspSessionState(ICommandRequest requestContext)
        {
            // building Cookie header with ASP's session cookies
            var aspSessionCookies = string.Join(";",
                requestContext.Cookies.Where(cookie => cookie.Key.StartsWith("ASPSESSIONID"))
                    .Select(cookie => string.Join("=", cookie.Key, cookie.Value)));

            if (aspSessionCookies.Length == 0)
            {
                // logs can be found in /ckfinder/App_Data/logs
                LoggerManager.GetLoggerForCurrentClass().Info("No ASP session cookie found");
                // don't make an extra request to the connector.asp, there's no session initiated
                return new JObject();
            }

            //replace this URL with your connector.asp's
            var publicAspSessionConnectorUrl = new Uri("http://myaspwebsite.com/connector.asp");
            var localSafeAspSessionConnectorUrl = new UriBuilder(publicAspSessionConnectorUrl) { Host = requestContext.LocalIpAddress };

            using (var wCli = new WebClient())
                try
                {
                    wCli.Headers.Add(HttpRequestHeader.Cookie, aspSessionCookies);
                    wCli.Headers.Add(HttpRequestHeader.Host, publicAspSessionConnectorUrl.Host);
                    return JObject.Parse(wCli.DownloadString(localSafeAspSessionConnectorUrl.Uri));
                }
                catch (Exception ex) // returning an empty JObject object in any fault
                {
                    // logs can be found in /ckfinder/App_Data/logs
                    LoggerManager.GetLoggerForCurrentClass().Error(ex);
                    return new JObject();
                }
        }

        public Task<IUser> AuthenticateAsync(ICommandRequest commandRequest, CancellationToken cancellationToken)
        {
            var aspSessionState = GetAspSessionState(commandRequest);

            var roles = new List<string>();
            var isEditor = aspSessionState.GetNullSafeValue("isEditor", false);
            var isMember = aspSessionState.GetNullSafeValue("isMember", false);

            if (isEditor) roles.Add("editor");
            if (isMember) roles.Add("member");

            var isAuthenticated = isEditor || isMember;
            var user = new User(isAuthenticated, roles);
            return Task.FromResult((IUser)user);
        }
    }

    public static class JObjectExtensions
    {
        // an extension method to help case insensitive lookups with a default value to get avoid NullReferenceException
        public static T GetNullSafeValue<T>(this JObject jobj, string key, T defaultValue = default(T))
        {
            dynamic val = jobj.GetValue(key, StringComparison.OrdinalIgnoreCase);
            if (val == null) return defaultValue;
            return (T)val;
        }
    }
}

Now you should have a working CKFinder connector. Change the logic in the method AuthenticateAsync if you need and see how CKFinder handles your Classic ASP membership management.

Kul-Tigin
  • 16,728
  • 1
  • 35
  • 64
  • This looks promising, Kul, thanks. I won't be able to test it until Tuesday, though (my home machine seems to be too old to run the necessary version of .NET). – Shaggy May 28 '16 at 15:57
  • I've just spotted that the bounty will expire before Tuesday. If I remember correctly, a portion of the bounty gets awarded to the answer with the most upvotes if it's not manually awarded. As yours is the best and most comprehensive answer I've received, I will try to temporarily remove my upvote on Lesmian's answer to allow that to happen. If your answer does indeed solve my problem, I will create a new bounty to award you the difference in rep. – Shaggy May 28 '16 at 16:13
  • @Shaggy Thanks for the thought, it's very kind of you. But it's really not necessary. I was too lazy to do this for a long time. Thanks to you I've just upgraded my own CKFinder 2.X instances to this new one. So you can assume I got my reward :) – Kul-Tigin May 28 '16 at 16:35
  • That did the trick, Kul, thank you so much. Answer accepted and bounty awarded. I am getting an error stating the "dropbox" file system isn't registered on the `config.LoadConfig()` line of the C# file that I'll need to figure out, but I can live without Dropbox for the time being. – Shaggy May 31 '16 at 11:07
  • @Shaggy I'm glad I could help. – Kul-Tigin May 31 '16 at 13:09
  • I've figured out how to fix the Dropbox error, if you'd like to add it to your answer. First, we need to add CKSource's `FileSystem.Dropbox` namespace and then, in the `RegisterFileSystems()` void, add a call to `FileSystemFactory.RegisterFileSystem()`. – Shaggy Jun 01 '16 at 11:20
  • @Shaggy done. generic connector also contains `AmazonStorage` and `AzureStorage` definitions in addition to `Dropbox` and `LocalStorage` BTW. – Kul-Tigin Jun 01 '16 at 11:31
  • 1
    I couldn't find any info in the [docs](http://docs.cksource.com/ckfinder3-net/namespace_c_k_source_1_1_file_system.html) about the Amazon & Azure `FileSystem` namespaces so, as I don't use them and couldn't test them, wasn't sure of the correct was to implement them which is why I left them out of my previous comment. – Shaggy Jun 01 '16 at 13:18
2

Did you setup your custom authentication provider with ConnectorBuilder?

public ConnectorBuilder ConfigureConnector()
{
   var connectorBuilder = new ConnectorBuilder();
   connectorBuilder.SetAuthenticator(new MyAuthenticator());

   return connectorBuilder;
}

You can find full example here: http://docs.cksource.com/ckfinder3-net/configuration_by_code.html.

UPDATE

Additionally you should register ConnectorBuilder inside Startup class to add it to request pipeline:

public void Configuration(IAppBuilder appBuilder)
{
   var connectorBuilder = ConfigureConnector();
   var connector = connectorBuilder.Build(new OwinConnectorFactory());
   appBuilder.Map("/CKFinder/connector", builder => builder.UseConnector(connector));
}

All this is from a documentation link I've provided before.

Shaggy
  • 6,696
  • 2
  • 25
  • 45
Lesmian
  • 3,932
  • 1
  • 17
  • 28
  • Thanks, Lesmian. However, after including that code and figuring out the additional namespaces that needed to be imported, it still doesn't work; CKF is still "disabled for security reasons". – Shaggy May 27 '16 at 10:40
  • Actually, if that function were required, shouldn't my original C# file have been throwing an error without it? – Shaggy May 27 '16 at 10:55
  • According to http://docs.cksource.com/ckfinder3-net/howto.html#howto_custom_authenticator you should implement interface and set it with ConnectorBuilder. By that I think it is required. How it would detect your custom implementation without it? Additionally you should register ConectorBuilder within Configuration(IAppBuilder appBuilder) method inside Startup class of your project. – Lesmian May 27 '16 at 11:01
  • And maybe your original C# code don't throw error because it doesn't see any custom authentication provider so it reverts to default one which is to block every request. – Lesmian May 27 '16 at 11:02
  • Thanks for your continued efforts, @Lesmian. I've updated my file with that additional code (see edit to question) but still can't get CKF to recognise the authentication. – Shaggy May 27 '16 at 11:14
  • Oh, now I see that I've misunderstood your question. You try to merge asp and asp.net which won't work as you think. In order to make that authentication to work you would need to run full asp.net website, not only parse html page ;( I think instead of trying to use asp.net mechanizm to block file access maybe you can use basic functionality of classic asp to restrict file access? – Lesmian May 27 '16 at 11:37
  • I was afraid that might be the case :( I currently have it set up to allow access to everyone but the pages it appears on require, at minimum, `isEditor` permissions. What I'm trying to do here is just add an extra level of security in case someone were to figure out the path to my "ckfinder.html" file and access it directly. I'll try to write some code for that C# file to query the database for permissions directly but how, then, would I integrate it into CKF? – Shaggy May 27 '16 at 11:49
  • I don't know much about vanilla asp but maybe you could use this technique to restrict access to your files: http://weblogs.asp.net/scottgu/tip-trick-integrating-asp-net-security-with-classic-asp-and-non-asp-net-urls. The point is to use asp.net security to restrict access to some file based on user authentication (you can use role based if you want). – Lesmian May 27 '16 at 12:03
  • Thanks for the link, @Lesmian, but it didn't make much sense to me (probably Friday code blindness!). I kept on with quesying my database directly from my C# file and, eventually, managed to come up with something that works (or, at least, doesn't throw any errors! I can't get anything to write to screen from within any of the functions to try to debug it.) so I just need to figure out how to tell CKF to check that file for authentication when it loads - any ideas there? – Shaggy May 27 '16 at 13:50