如何使用 Azure AD / OpenId 进行身份验证但使用基于 Entity Framework 的 user/role 数据

How to authenticate with Azure AD / OpenId but use Entity Framework based user/role data

我正在尝试改进遗留 ASPNet MVC/OWIN 应用程序的身份验证故事 - 目前,它使用 AspNetUsers / AspNetRoles / claims 等表以及表单 + 基于 cookie 的身份验证。

我想使用 Azure AD / OpenID Connect 进行身份验证,但随后从数据库中加载用户 profile/roles 当前。基本上,应用程序内不再有密码管理。用户自己仍然需要 exist/be 在应用程序中创建。

该应用程序非常依赖与这些用户关联的一些自定义数据,因此简单地使用 Active Directory 中的角色不是一种选择。

OpenID 身份验证有效,但我不确定如何将现有的 Identityuser / IdentityUserRole / RoleManager 管道与其结合使用。

基本上,一旦用户使用 Open ID 进行身份验证,我们就会希望从数据库中加载相应的用户(匹配电子邮件地址)并继续使用该用户配置文件/角色。

特别是,AuthorizeAttribute(指定了特定角色)应该像以前一样继续发挥作用。

这是我目前拥有的:

public class IdentityConfig
{
    public void Configuration(IAppBuilder app)
    {
        app.CreatePerOwinContext(AppIdentityDbContext.Create);
        app.CreatePerOwinContext<AppUserManager>(AppUserManager.Create);
        app.CreatePerOwinContext<AppRoleManager>(AppRoleManager.Create);

        ConfigureAuth(app);
    }

    /// <summary>
    /// Configures OpenIDConnect Authentication & Adds Custom Application Authorization Logic on User Login.
    /// </summary>
    /// <param name="app">The application represented by a <see cref="IAppBuilder"/> object.</param>
    private void ConfigureAuth(IAppBuilder app)
    {
        app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

        app.UseCookieAuthentication(new CookieAuthenticationOptions());

        //Configure OpenIDConnect, register callbacks for OpenIDConnect Notifications
        app.UseOpenIdConnectAuthentication(
            new OpenIdConnectAuthenticationOptions
            {
                ClientId = ConfigHelper.ClientId,
                Authority = String.Format(CultureInfo.InvariantCulture, ConfigHelper.AadInstance,
                    ConfigHelper.Tenant), // For Single-Tenant
                PostLogoutRedirectUri = ConfigHelper.PostLogoutRedirectUri,

                TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters
                {
                    RoleClaimType = "roles",
                },

                Notifications = new OpenIdConnectAuthenticationNotifications
                {
                    AuthenticationFailed = context =>
                    {
                        context.HandleResponse();
                        context.Response.Redirect("/Error/OtherError?errorDescription=" +
                                                  context.Exception.Message);
                        return Task.FromResult(0);
                    },
                    SecurityTokenValidated = async context =>
                    {
                        string userIdentityName = context.AuthenticationTicket.Identity.Name;
                        var userManager = context.OwinContext.GetUserManager<AppUserManager>();
                        var user = userManager.FindByEmail(userIdentityName);
                        if (user == null)
                        {
                            Log.Error("User {name} authenticated with open ID, but unable to find matching user in store", userIdentityName);
                            context.HandleResponse();
                            context.Response.Redirect("/Error/NoAccess?identity=" + userIdentityName);
                            return;
                        }

                        user.DateLastLogin = DateTime.Now;
                        IdentityResult result = await userManager.UpdateAsync(user);

                        if (result.Succeeded)
                        {
                            var authManager = context.OwinContext.Authentication;
                            ClaimsIdentity ident = await userManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ExternalBearer);


                            // Attach additional claims from DB user
                            authManager.User.AddIdentity(ident);

                            // authManager.SignOut();
                            // authManager.SignIn(new AuthenticationProperties { IsPersistent = false }, ident);

                            return;
                        }

                        throw new Exception(string.Format("Failed to update user {0} after log-in", userIdentityName));
                    }
                }
            });
    }
}

The OpenID auth works, however I'm not sure how to use the existing Identityuser / IdentityUserRole / RoleManager plumbing in conjunction with it.

The application is quite dependent on some custom data associated with these users so simply using the roles from Active Directory isn't an option.

根据您的要求,我假设您可以构建您的身份服务器(例如 IdentityServer3) and leverage IdentityServer3.AspNetIdentity 使用 ASP.NET Identity 进行身份管理。

对于您的 Web 客户端应用程序,您可以使用 OpenID Connect 中间件并将 Authority 设置为您的自定义身份服务器,并在您的身份服务器上设置预配置的 ClientId。

另外,你可以关注这个tutorial for quickly getting started with IdentityServer3 and the full samples IdentityServer3 Samples

这是我最后做的事情:

public class IdentityConfig
{
    public void Configuration(IAppBuilder app)
    {
        app.CreatePerOwinContext(AppIdentityDbContext.Create);
        app.CreatePerOwinContext<AppUserManager>(AppUserManager.Create);
        app.CreatePerOwinContext<AppRoleManager>(AppRoleManager.Create);
        ConfigureAuth(app);
    }

    /// <summary>
    /// Configures OpenIDConnect Authentication & Adds Custom Application Authorization Logic on User Login.
    /// </summary>
    /// <param name="app">The application represented by a <see cref="IAppBuilder"/> object.</param>
    private void ConfigureAuth(IAppBuilder app)
    {
        app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

        app.UseCookieAuthentication(new CookieAuthenticationOptions
        {
            CookieDomain = ConfigHelper.AuthCookieDomain,
            SlidingExpiration = true,
            ExpireTimeSpan = TimeSpan.FromHours(2)
        });

        //Configure OpenIDConnect, register callbacks for OpenIDConnect Notifications
        app.UseOpenIdConnectAuthentication(
            new OpenIdConnectAuthenticationOptions
            {
                ClientId = ConfigHelper.ClientId,
                Authority = String.Format(CultureInfo.InvariantCulture, ConfigHelper.AadInstance, ConfigHelper.Tenant),

                TokenValidationParameters = new System.IdentityModel.Tokens.TokenValidationParameters
                {
                    RoleClaimType = ClaimTypes.Role
                },

                Notifications = new OpenIdConnectAuthenticationNotifications
                {
                    AuthenticationFailed = context =>
                    {
                        context.HandleResponse();
                        context.Response.Redirect("/Error/OtherError?errorDescription=" + context.Exception.Message);
                        return Task.FromResult(0);
                    },
                    RedirectToIdentityProvider = context =>
                    {
                        // Set the post-logout & redirect URI dynamically depending on the incoming request.
                        // That allows us to use the same Azure AD app for two subdomains (these two domains give different app behaviour)
                        var builder = new UriBuilder(context.Request.Uri);
                        builder.Fragment = builder.Path = builder.Query = "";
                        context.ProtocolMessage.PostLogoutRedirectUri = builder.ToString();
                        context.ProtocolMessage.RedirectUri = builder.ToString();
                        return Task.FromResult(0);
                    }
                }
            });

        app.Use<EnrichIdentityWithAppUserClaims>();
    }
}

public class EnrichIdentityWithAppUserClaims : OwinMiddleware
{
    public EnrichIdentityWithAppUserClaims(OwinMiddleware next) : base(next)
    {
    }

    public override async Task Invoke(IOwinContext context)
    {
        await MaybeEnrichIdentity(context);
        await Next.Invoke(context);
    }

    private async Task MaybeEnrichIdentity(IOwinContext context)
    {
        ClaimsIdentity openIdUserIdentity = (ClaimsIdentity)context.Authentication.User.Identity;
        string userIdentityName = openIdUserIdentity.Name;

        var userManager = context.GetUserManager<AppUserManager>();
        var appUser = userManager.FindByEmail(userIdentityName);

        if (appUser == null)
        {
            Log.Error("User {name} authenticated with open ID, but unable to find matching user in store", userIdentityName);
            return;
        }

        appUser.DateLastLogin = DateTime.Now;
        IdentityResult result = await userManager.UpdateAsync(appUser);
        if (result.Succeeded)
        {
            ClaimsIdentity appUserIdentity = await userManager.CreateIdentityAsync(appUser, DefaultAuthenticationTypes.ExternalBearer);
            openIdUserIdentity.AddClaims(appUserIdentity.Claims);
        }
    }
}

它与我原来的非常相似 -(注意:RoleClaimType = ClaimTypesRoles 而不是 "roles")除了我没有尝试在 SecurityTokenValidated 回调中处理用户,我已经添加了一些自定义中间件,用于查找匹配用户(通过电子邮件地址)并将匹配应用用户的声明(应用角色)添加到经过身份验证的用户身份(OpenID 身份)。

最后,我使用(自定义)AuthorizeAttribute(此处未显示)保护所有控制器操作,确保经过身份验证的用户至少属于 "User" 角色(如果不是,则将他们重定向到"no access" 页面表明我们已经对他们进行了身份验证,但他们在系统中没有访问权限。