21

I want to use NancyFx for an intranet web app. All the documentation and forums only mention Forms and Basic authentication. Anyone successfully use Nancy with Windows Authentication?

There's also something called Nancy.Authentication.Stateless but I can't see what that does (looks like it's for use in Apis).

PPC-Coder
  • 3,522
  • 2
  • 21
  • 30

5 Answers5

18

Using Nancy with WindowsAuthentication is discussed by this thread. Damian Hickey has provided an example of using Nancy, hosted by OWin with WindowsAuthentication.

I have slightly modified the code (to remove the now deprecated NancyOwinHost):

namespace ConsoleApplication1
{
    using System;
    using System.Net;
    using System.Security.Principal;
    using Microsoft.Owin.Hosting;
    using Nancy;
    using Nancy.Owin;
    using Owin;

    internal static class Program
    {
        private static void Main(string[] args)
        {
            using (WebApp.Start<Startup>("http://localhost:9000"))
            {
                Console.WriteLine("Press any key to quit.");
                Console.ReadKey();
            }
        }
    }

    internal sealed class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            var listener = (HttpListener) app.Properties["System.Net.HttpListener"];
            listener.AuthenticationSchemes = AuthenticationSchemes.IntegratedWindowsAuthentication;

            app.UseNancy();
        }
    }

    public sealed class MyModule : NancyModule
    {
        public MyModule()
        {
            Get[""] = _ =>
            {
                var env = this.Context.GetOwinEnvironment();
                var user = (IPrincipal) env["server.User"];

                return "Hello " + user.Identity.Name;
            };
        }
    }
}

Special thanks go to Damian!


The example requires the following NuGet packages:

  • Microsoft.Owin.Host.HttpListener
  • Microsoft.Owin.Hosting
  • Microsoft.Owin
  • Nancy
  • Nancy.Owin
  • Owin
CodeFox
  • 3,321
  • 1
  • 29
  • 41
17

I needed Windows Authentication with Nancy for a basic intranet application. I used @Steven Robbins answer as a starting point, but stripped away things we didn't need and then added population of the NancyContext.CurrentUser property.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Principal;
using System.Web;
using Nancy;
using Nancy.Security;

namespace YourNamespace
{
    /// <summary>
    /// Extensions for Nancy that implement Windows Authentication.
    /// </summary>
    public static class WindowsAuthenticationExtensions
    {
        private class WindowsUserIdentity : IUserIdentity
        {
            private string _userName;

            public WindowsUserIdentity(string userName)
            {
                _userName = userName;
            }

            #region IUserIdentity

            IEnumerable<string> IUserIdentity.Claims
            {
                get { throw new NotImplementedException(); }
            }

            string IUserIdentity.UserName
            {
                get { return _userName; }
            }

            #endregion
        }

        #region Methods

        /// <summary>
        /// Forces the NancyModule to require a user to be Windows authenticated. Non-authenticated
        /// users will be sent HTTP 401 Unauthorized.
        /// </summary>
        /// <param name="module"></param>
        public static void RequiresWindowsAuthentication(this NancyModule module)
        {
            if (HttpContext.Current == null) 
                throw new InvalidOperationException("An HttpContext is required. Ensure that this application is running under IIS.");

            module.Before.AddItemToEndOfPipeline(
                new PipelineItem<Func<NancyContext, Response>>(
                    "RequiresWindowsAuthentication",
                    context =>
                    {
                        var principal = GetPrincipal();

                        if (principal == null || !principal.Identity.IsAuthenticated)
                        {
                            return HttpStatusCode.Unauthorized;
                        }

                        context.CurrentUser = new WindowsUserIdentity(principal.Identity.Name);

                        return null;
                    }));
        }

        private static IPrincipal GetPrincipal()
        {
            if (HttpContext.Current != null)
            {
                return HttpContext.Current.User;
            }

            return new WindowsPrincipal(WindowsIdentity.GetCurrent());
        }

        #endregion

    }
}

You use it like this:

public class YourModule : NancyModule
{
    public YourModule()
    {
        this.RequiresWindowsAuthentication();

        Get["/"] = parameters =>
            {
                //...
            };
    }

}

John Mills
  • 10,020
  • 12
  • 74
  • 121
  • Thanks, this helped me a lot. How could you amend this so it's per request rather than module level? Or would you just check the individual claims within each route? – mjbates7 Oct 29 '13 at 12:13
  • You can add a [this.RequiresAuthentication()](https://stackoverflow.com/questions/12185257/nancyfx-authentication-per-route) inside of the route handler. – sean_m Feb 11 '15 at 00:59
  • 1
    Not really helpful in the case of self-hosting in `OWIN` as you will be tied to `System.Web` the answer by `CodeFox` met my requirements. – MaYaN Nov 23 '15 at 15:20
8

I used this in an internal project recently - I don't really like it, and it ties you to asp.net hosting, but it did the job:

namespace Blah.App.Security
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Security.Principal;
    using System.Web;

    using Nancy;

    public static class SecurityExtensions
    {
        public static string CurrentUser
        {
            get
            {
                return GetIdentity().Identity.Name;
            }
        }

        public static bool HasRoles(params string[] roles)
        {
            if (HttpContext.Current != null && HttpContext.Current.Request.IsLocal)
            {
                return true;
            }

            var identity = GetIdentity();

            return !roles.Any(role => !identity.IsInRole(role));
        }

        public static void RequiresWindowsAuthentication(this NancyModule module)
        {
            if (HttpContext.Current != null && HttpContext.Current.Request.IsLocal)
            {
                return;
            }

            module.Before.AddItemToEndOfPipeline(
                new PipelineItem<Func<NancyContext, Response>>(
                    "RequiresWindowsAuthentication",
                    ctx =>
                        {
                            var identity = GetIdentity();

                            if (identity == null || !identity.Identity.IsAuthenticated)
                            {
                                return HttpStatusCode.Forbidden;
                            }

                            return null;
                        }));
        }

        public static void RequiresWindowsRoles(this NancyModule module, params string[] roles)
        {
            if (HttpContext.Current != null && HttpContext.Current.Request.IsLocal)
            {
                return;
            }

            module.RequiresWindowsAuthentication();

            module.Before.AddItemToEndOfPipeline(new PipelineItem<Func<NancyContext, Response>>("RequiresWindowsRoles", GetCheckRolesFunction(roles)));
        }

        private static Func<NancyContext, Response> GetCheckRolesFunction(IEnumerable<string> roles)
        {
            return ctx =>
                {
                    var identity = GetIdentity();

                    if (roles.Any(role => !identity.IsInRole(role)))
                    {
                        return HttpStatusCode.Forbidden;
                    }

                    return null;
                };
        }

        private static IPrincipal GetIdentity()
        {
            if (System.Web.HttpContext.Current != null)
            {
                return System.Web.HttpContext.Current.User;
            }

            return new WindowsPrincipal(WindowsIdentity.GetCurrent());
        }

        public static Func<NancyContext, Response> RequireGroupForEdit(string group)
        {
            return ctx =>
                {
                    if (ctx.Request.Method == "GET")
                    {
                        return null;
                    }

                    return HasRoles(group) ? null : (Response)HttpStatusCode.Forbidden;
                };
        }
    }
}

It bypasses all the security checks if it's coming from local (for testing), which is probably a bad idea, but it's a behind the firewall thing so it isn't an issue for this.

Wouldn't suggest you use it verbatim, but might point you in the right direction :)

Steven Robbins
  • 26,441
  • 7
  • 76
  • 90
2

You could try to help me finish Nancy.Authentication.Ntlm. This is definitely pre-alpha. I don't know how to implement several thing based mostly on my limited knowledge of Nancy internals.

Currently the code challenges client, validates the answer. But I failed inform client about success of this operation.

But I still working hard. Really hard.

And I would appreciate your comments and pull requests if any.

shytikov
  • 9,155
  • 8
  • 56
  • 103
2

Standing on the sholders of giants, I've implemented it in this way to allow the authentication to be mocked for testing

using System;
using System.Collections.Generic;
using Nancy;
using Nancy.Security;

namespace Your.Namespace
{
    /// <summary>
    /// Extensions for Nancy that implement Windows Authentication.
    /// </summary>
    public static class WindowsAuthenticationExtensions
    {
        private class WindowsUserIdentity : IUserIdentity
        {
            private readonly string _userName;

            public WindowsUserIdentity(string userName)
            {
                _userName = userName;
            }

            #region IUserIdentity

            IEnumerable<string> IUserIdentity.Claims
            {
                get { throw new NotImplementedException(); }
            }

            string IUserIdentity.UserName
            {
                get { return _userName; }
            }

            #endregion
        }

        #region Methods

        /// <summary>
        /// Forces the NancyModule to require a user to be Windows authenticated. Non-authenticated
        /// users will be sent HTTP 401 Unauthorized.
        /// </summary>
        /// <param name="module"></param>
        /// <param name="authenticationProvider"></param>
        public static void RequiresWindowsAuthentication(this NancyModule module, IWindowsAuthenticationProvider authenticationProvider)
        {
            if (!authenticationProvider.CanAuthenticate)
                throw new InvalidOperationException("An HttpContext is required. Ensure that this application is running under IIS.");

            module.Before.AddItemToEndOfPipeline(
                new PipelineItem<Func<NancyContext, Response>>(
                    "RequiresWindowsAuthentication",
                    context =>
                    {
                        var principal = authenticationProvider.GetPrincipal();

                        if (principal == null || !principal.Identity.IsAuthenticated)
                        {
                            return HttpStatusCode.Unauthorized;
                        }

                        context.CurrentUser = new WindowsUserIdentity(principal.Identity.Name);

                        return null;
                    }));
        }

        #endregion

    }
}

IWindowsAuthenticationProvider:

using System.Security.Principal;

namespace Your.Namespace
{
    public interface IWindowsAuthenticationProvider
    {
        bool CanAuthenticate { get; }
        IPrincipal GetPrincipal();
    }
}

WindowsAuthenticationProvider:

using System.Security.Principal;
using System.Web;

namespace Your.Namespace
{
    public class WindowsAuthenticationProvider : IWindowsAuthenticationProvider
    {
        public bool CanAuthenticate
        {
            get { return HttpContext.Current != null; }
        }

        public IPrincipal GetPrincipal()
        {
            if (HttpContext.Current != null)
            {
                return HttpContext.Current.User;
            }

            return new WindowsPrincipal(WindowsIdentity.GetCurrent());
        }
    }
}

Implementing it is a little messy as you need the IWindowsAuthenticationProvided injected into every module

public DefaultModule(IWindowsAuthenticationProvider authenticationProvider) 
        {
            this.RequiresWindowsAuthentication(authenticationProvider);
            Get["/"] = _ => "Hello World";
        }
Fran Hoey
  • 895
  • 9
  • 18