I know that blog post title is sure a mouth-full, but it describes the whole problem I was trying to solve in a recent project.

The Project

Let me outline the project briefly.  We were building a report dashboard-type site that will live inside the client’s network.  The dashboard gives an overview of various, very important information that relates to how the company is performing on a hourly basis.  So, the dashboard is only available to a certain group of directors.

To limit the solution to the these directors, authentication and authorization would go through their existing Active Directory setup by putting the authorized users in a special AD group.

The Problem

Getting authentication to work was a snap.  Microsoft provides theSystem.Web.Security.ActiveDirectoryMembershipProvider
class to use as your membership provider.  Putting an [Authorize] attribute on my action methods or entire controllers was all I needed to get it working (besides, of course, the system.web/authentication web.config updates and a controller to show my login form and handle the submit credentials).

Here’s my relevant web.config setup:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<connectionStrings>
  <add name="ADConnectionString" connectionString="<ldap connection string here>" />
</connectionStrings>
...
<authentication mode="Forms">
  <forms name=".AuthCookie" loginUrl="~/login"/>
</authentication>
<membership defaultProvider="ADMembershipProvider">
  <providers>
    <clear/>
    <add name="ADMembershipProvider"
         type="System.Web.Security.ActiveDirectoryMembershipProvider"
         connectionStringName="ADConnectionString"
         attributeMapUsername="sAMAccountName"/>
  </providers>
</membership>

The tough part came when I wanted to limit access to users in that AD group. Microsoft doesn’t provide a RoleProvider along with its ActiveDirectoryMembershipProvider. So, what to do?

I tried several methods I found online. Most of them were based on creating my own customRoleProvider and querying AD to iterate through the user’s groups (treating them like roles) and seeing if one of them matched my AD group I was looking for. However, I could never get it to work. Each code example I found eventually gave me this AD error when I iterated through the current user’s AD groups:

1
The specified directory service attribute or value does not exist.

The Solution

Eventually, I found a solution online that worked. Instead of setting up a custom RoleProvider, all it involved was creating a custom AuthorizeAttribute for your MVC controllers (or action methods) that checked the user’s .IsMemberOf method to see if the member belonged the sought after group (or groups). I don’t know why this method does not cause the same AD error as describe above, but I’m glad it doesn’t! All I can assume is that it queries AD in a more friendly way.

Here is my custom AuthorizeAttribute:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class AuthorizeADAttribute : AuthorizeAttribute
{
    private bool _authenticated;
    private bool _authorized;
 
    public string Groups { get; set; }
 
    protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
    {
        base.HandleUnauthorizedRequest(filterContext);
 
        if (_authenticated && !_authorized)
        {
            filterContext.Result = new RedirectResult("/error/notauthorized");
        }
    }
 
    protected override bool AuthorizeCore(HttpContextBase httpContext)
    {
        _authenticated = base.AuthorizeCore(httpContext);
 
        if (_authenticated)
        {
            if (string.IsNullOrEmpty(Groups))
            {
                _authorized = true;
                return _authorized;
            }
 
            var groups = Groups.Split(',');
            string username = httpContext.User.Identity.Name;
 
            try
            {
                _authorized = LDAPHelper.UserIsMemberOfGroups(username, groups);
                return _authorized;
            }
            catch (Exception ex)
            {
                this.Log().Error(() => "Error attempting to authorize user", ex);
                _authorized = false;
                return _authorized;
            }
        }
 
        _authorized = false;
        return _authorized;
    }
}

Notice that I also included a little code to distinguish between the user not being authenticated (which the call to base.AuthorizeCore takes care of) and not being authorized. Without the code inHandleUnauthorizedRequest, if the user successfully logs in but is not in the AD group, he just sees the log in screen again which doesn’t communicate the problem very well.

The this.Log() code uses a Nuget packaged called this.Log. The LDAPHelper class is something I wrote. The code is below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public static class LDAPHelper
{
    public static string GetLDAPContainer()
    {
        Uri ldapUri;
        ParseLDAPConnectionString(out ldapUri);
 
        return HttpUtility.UrlDecode(ldapUri.PathAndQuery.TrimStart('/'));
    }
 
    public static string GetLDAPHost()
    {
        Uri ldapUri;
        ParseLDAPConnectionString(out ldapUri);
 
        return ldapUri.Host;
    }
 
    public static bool ParseLDAPConnectionString(out Uri ldapUri)
    {
        string connString = ConfigurationManager.ConnectionStrings["ADConnectionString"].ConnectionString;
 
        return Uri.TryCreate(connString, UriKind.Absolute, out ldapUri);
    }
 
    public static bool UserIsMemberOfGroups(string username, string[] groups)
    {
        /* Return true immediately if the authorization is not
        locked down to any particular AD group */
        if (groups == null || groups.Length == 0)
        {
            return true;
        }
 
        // Verify that the user is in the given AD group (if any)
        using (var context = BuildPrincipalContext())
        {
            var userPrincipal = UserPrincipal.FindByIdentity(context,
                                                 IdentityType.SamAccountName,
                                                 username);
 
            foreach (var group in groups)
            {
                if (userPrincipal.IsMemberOf(context, IdentityType.Name, group))
                {
                    return true;
                }
            }
        }
 
        return false;
    }
 
    public static PrincipalContext BuildPrincipalContext()
    {
        string container = LDAPHelper.GetLDAPContainer();
        return new PrincipalContext(ContextType.Domain, null, container);
    }
}

My code is mostly based on example code I found on a very helpful StackOverflow post:http://stackoverflow.com/questions/4342271/asp-net-mvc-forms-authorization-with-active-directory-groups/4383502#4383502.

To use this code, all you have to do is use your custom AuthorizeAttribute instead of the built-in one. Something like this:

1
2
3
4
5
[AuthorizeAD(Groups="Some AD group name")]
public class HomeController : Controller
{
...
}

Active Directory Authentication in ASP.NET MVC 5 with Forms Authentication and Group-Based Authorization的更多相关文章

  1. Forms Authentication in ASP.NET MVC 4

    原文:Forms Authentication in ASP.NET MVC 4 Contents: Introduction Implement a custom membership provid ...

  2. [转]Implementing User Authentication in ASP.NET MVC 6

    本文转自:http://www.dotnetcurry.com/aspnet-mvc/1229/user-authentication-aspnet-mvc-6-identity In this ar ...

  3. Asp.Net MVC 身份验证-Forms

    Asp.Net MVC 身份验证-Forms 在MVC中对于需要登录才可以访问的页面,只需要在对应的Controller或Action上添加特性[Authorize]就可以限制非登录用户访问该页面.那 ...

  4. Migrating an ASP.NET MVC application to ADFS authentication

    I recently built an ASP.NET application at work to help track internal use of our products. It's bee ...

  5. 简化 Web 应用程序与 Windows Azure Active Directory、ASP.NET 和 Visual Studio 的集成

    大家好! 今天的博文深入讨论我们今天推出的开发人员工具和框架中的一些新功能.我们通过与 ASP.NET 和 Visual Studio 团队合作开发了一些重大的增强功能,让开发人员能够轻松使用 Win ...

  6. Winbind authentication against active directory

    Winbind authentication against active directory Description This tip will describe how to configure ...

  7. ASP.NET MVC 随想录——探索ASP.NET Identity 身份验证和基于角色的授权,中级篇

    在前一篇文章中,我介绍了ASP.NET Identity 基本API的运用并创建了若干用户账号.那么在本篇文章中,我将继续ASP.NET Identity 之旅,向您展示如何运用ASP.NET Ide ...

  8. ASP.NET MVC 随想录——开始使用ASP.NET Identity,初级篇

    在之前的文章中,我为大家介绍了OWIN和Katana,有了对它们的基本了解后,才能更好的去学习ASP.NET Identity,因为它已经对OWIN 有了良好的集成. 在这篇文章中,我主要关注ASP. ...

  9. ASP.NET MVC 随想录——开始使用ASP.NET Identity,初级篇(转)

    ASP.NET MVC 随想录——开始使用ASP.NET Identity,初级篇   阅读目录 ASP.NET Identity 前世今生 建立 ASP.NET Identity 使用ASP.NET ...

随机推荐

  1. php 允许浏览器跨域访问web服务端的解决方案

    今天和同事探讨了前后端如何真正实现隔离开发的问题,如果前端单独作为服务发布,势必会涉及到无法直接调用后端的接口的问题,因为浏览器是不允许跨域提交请求的. 所谓跨域访问,就是在浏览器窗口,和某个服务端通 ...

  2. 潭州课堂25班:Ph201805201 爬虫基础 第十三课 cookie (课堂笔记)

    # -*- coding: utf-8 -*- # 斌彬电脑 # @Time : 2018/9/15 0015 4:52 #cookie 是服务器发给浏览器的特殊信息 # 可以理解为一个临时通行证 # ...

  3. API使用

    至于什么是API我想不用累述了,百科上面有,其实就是别人写好了一大堆功能性的代码,然后你可以拿来用.一般的二次开发都是使用api来开发,包括现在的高级程序设计,很少自己写基本代码了,像.netFram ...

  4. memcached 数据库

    安装 [root@node2 ~]# yum install memcached 启动 [root@node2 ~]# systemctl start memcached 连接 [root@node2 ...

  5. unity中尽量规避的C#写法

    首先想到的也就是重名的问题. 创建C#脚本的时候,引擎会搜索所有的文件夹里已有的C#脚本,若已经存在这个名字的脚本文件,那么系统就会报错:    再者,类名和方法名不能相同,理解来就是,方法名和类名相 ...

  6. 微软官方的.net命令行参数库

    虽然现在是图形化大行其道的时代,但让程序支持命令行启动对于专业的领域还是有不少需求的..net本身并没有内置对命令行解析的支持,我之前就写过一篇文章让.Net程序支持命令行启动介绍了几个第三方的命令行 ...

  7. hdu4607Park Visit 树的直径

    //给一棵双向树,数中边的权值为1,问对于这颗树中走k个节点的最短路径 //假设k小于这颗数的直径加1,那么走k个节点就没有反复的路,假设大于 //那么大于的节点都须要走两遍 #include< ...

  8. Selenium support for PhantomJS has been deprecated, please use headless

    今天在使用Selenuim+PhantomJS动态抓取网页时,出现如下报错信息: C:\Python36\lib\site-packages\selenium-3.11.0-py3.6.egg\sel ...

  9. Convolutional Neural Networks: Application

    Andrew Ng deeplearning courese-4:Convolutional Neural Network Convolutional Neural Networks: Step by ...

  10. ThinkPhp 更改 BIT 类型的问题

    在使用ThinkPhp更改字段为BIT 类型的注意了,您将会遇到以下错误信息:  这是因为ThinkPhp在使用setField或Save方法时,将你的值更改成了字符串类型 而TINYINT 类型是不 ...