如何使用 Azure AD / OpenId 进行身份验证但使用基于实体框架的用户/角色数据

edd*_*uld 5 entity-framework owin asp.net-identity azure-active-directory openid-connect

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

我想使用 Azure AD / OpenID Connect 进行身份验证,然后像当前一样从数据库加载用户配置文件/角色。基本上,应用程序内不再有密码管理。用户本身仍然需要在应用程序中存在/创建。

该应用程序非常依赖于与这些用户关联的一些自定义数据,因此简单地使用 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));
                    }
                }
            });
    }
}
Run Code Online (Sandbox Code Playgroud)

edd*_*uld 5

这就是我最终所做的:

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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

它与我最初的非常相似 - (注意:RoleClaimType = ClaimTypesRoles不是“角色”)除了尝试在回调中处理用户之外SecurityTokenValidated,我添加了一些自定义中间件来查找匹配的用户(通过电子邮件地址)并添加从匹配的应用程序用户到经过身份验证的用户身份(OpenID 身份)的声明(应用程序角色)。

最后,我使用(自定义)AuthorizeAttribute(此处未显示)保护所有控制器操作,以确保经过身份验证的用户至少属于“用户”角色(如果不是,则将它们重定向到“无访问”页面,表明我们已通过身份验证他们,但他们无权访问系统)。