Asp Core:Azure Ad Auth +自定义JWT +自定义标识存储

时间:2018-04-05 09:24:37

标签: c# azure asp.net-core .net-core jwt

使用ASP.NET Core 2.0,我尝试实现以下目标:

  1. 通过Azure AD(已注册的应用程序)进行身份验证
  2. 自定义JWT作为身份验证方案
    • 使网络应用程序在服务器/实例之间进行身份验证
    • 能够使用桌面客户​​端保存持有者登录
  3. 拥有自定义标识存储库以引入自定义角色,策略和其他。
  4. 所有这些部分都有工作示例,但在尝试合并它们时,我偶然发现了一些问题。

    Web Api + Azure Ad Auth示例使用JWT令牌进行身份验证,但没有用于验证或创建令牌的逻辑。它既没有登录/注销的逻辑,但这似乎是合理的,它只是Api。

    以下是Web Api示例代码的快速提醒:

    AzureAdAuthenticationBuilderExtensions.cs

    using System;
    using Microsoft.AspNetCore.Authentication.JwtBearer;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Options;
    
    namespace Microsoft.AspNetCore.Authentication
    {
        public static class AzureAdServiceCollectionExtensions
        {
            public static AuthenticationBuilder AddAzureAdBearer(this AuthenticationBuilder builder)
                => builder.AddAzureAdBearer(_ => { });
    
            public static AuthenticationBuilder AddAzureAdBearer(this AuthenticationBuilder builder, Action<AzureAdOptions> configureOptions)
            {
                builder.Services.Configure(configureOptions);
                builder.Services.AddSingleton<IConfigureOptions<JwtBearerOptions>, ConfigureAzureOptions>();
                builder.AddJwtBearer();
                return builder;
            }
    
            private class ConfigureAzureOptions: IConfigureNamedOptions<JwtBearerOptions>
            {
                private readonly AzureAdOptions _azureOptions;
    
                public ConfigureAzureOptions(IOptions<AzureAdOptions> azureOptions)
                {
                    _azureOptions = azureOptions.Value;
                }
    
                public void Configure(string name, JwtBearerOptions options)
                {
                    options.Audience = _azureOptions.ClientId;
                    options.Authority = $"{_azureOptions.Instance}{_azureOptions.TenantId}";
                }
    
                public void Configure(JwtBearerOptions options)
                {
                    Configure(Options.DefaultName, options);
                }
            }
        }
    }
    

    摘录 Startup.cs

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication(sharedOptions =>
        {
            sharedOptions.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
        })
        .AddAzureAdBearer(options => Configuration.Bind("AzureAd", options));
    
        services.AddMvc();
    }
    

    另一方面,Web应用程序+ Azure广告示例使用带有cookie的OpenId,并且具有登录/注销逻辑:

    AzureAdAuthenticationBuilderExtensions.cs

    using System;
    using Microsoft.AspNetCore.Authentication.OpenIdConnect;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Options;
    
    namespace Microsoft.AspNetCore.Authentication
    {
        public static class AzureAdAuthenticationBuilderExtensions
        {
            public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder)
                => builder.AddAzureAd(_ => { });
    
            public static AuthenticationBuilder AddAzureAd(this AuthenticationBuilder builder, Action<AzureAdOptions> configureOptions)
            {
                builder.Services.Configure(configureOptions);
                builder.Services.AddSingleton<IConfigureOptions<OpenIdConnectOptions>, ConfigureAzureOptions>();
                builder.AddOpenIdConnect();
                return builder;
            }
    
            private class ConfigureAzureOptions : IConfigureNamedOptions<OpenIdConnectOptions>
            {
                private readonly AzureAdOptions _azureOptions;
    
                public ConfigureAzureOptions(IOptions<AzureAdOptions> azureOptions)
                {
                    _azureOptions = azureOptions.Value;
                }
    
                public void Configure(string name, OpenIdConnectOptions options)
                {
                    options.ClientId = _azureOptions.ClientId;
                    options.Authority = $"{_azureOptions.Instance}{_azureOptions.TenantId}";
                    options.UseTokenLifetime = true;
                    options.CallbackPath = _azureOptions.CallbackPath;
                    options.RequireHttpsMetadata = false;
                }
    
                public void Configure(OpenIdConnectOptions options)
                {
                    Configure(Options.DefaultName, options);
                }
            }
        }
    }
    

    摘录 Startup.cs

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication(sharedOptions =>
        {
            sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
        })
        .AddAzureAd(options => Configuration.Bind("AzureAd", options))
        .AddCookie();
    
        services.AddMvc(options =>
        {
            var policy = new AuthorizationPolicyBuilder()
                .RequireAuthenticatedUser()
                .Build();
            options.Filters.Add(new AuthorizeFilter(policy));
        })
        .AddRazorPagesOptions(options =>
        {
            options.Conventions.AllowAnonymousToFolder("/Account");
        });
    }
    

    AccountController.cs

    public class AccountController : Controller
    {
        [HttpGet]
        public IActionResult SignIn()
        {
            var redirectUrl = Url.Page("/Index");
            return Challenge(
                new AuthenticationProperties { RedirectUri = redirectUrl },
                OpenIdConnectDefaults.AuthenticationScheme
            );
        }
    
        [HttpGet]
        public IActionResult SignOut()
        {
            var callbackUrl = Url.Page("/Account/SignedOut", pageHandler: null, values: null, protocol: Request.Scheme);
            return SignOut(
                new AuthenticationProperties { RedirectUri = callbackUrl },
                CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectDefaults.AuthenticationScheme
            );
        }
    }
    

    我已经以某种方式合并了两种变体,但显然它并不起作用。我在登录方法中将CookieAuthenticationDefault替换为JwtBearerDefaults

    摘录 Startup.cs

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddEntityFrameworkSqlServer().AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
    
        services.AddIdentity<ApplicationUser, IdentityRole>()
            .AddEntityFrameworkStores<ApplicationDbContext>()
            .AddDefaultTokenProviders();
    
        services.AddAuthentication(sharedOptions =>
        {
            sharedOptions.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
            sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
        })
        .AddAzureAd(options => Configuration.Bind("AzureAd", options))
        .AddJwtBearer(options =>
        {
            options.IncludeErrorDetails = true;
    
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidateAudience = true,
                ValidateLifetime = true,
                ValidateIssuerSigningKey = true,
                ValidIssuer = "localhost",
                ValidAudience = "localhost",
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("test"))
            };
        });
    
        services.AddMvc(options =>
        {
            var policy = new AuthorizationPolicyBuilder()
                .RequireAuthenticatedUser()
                .Build();
            options.Filters.Add(new AuthorizeFilter(policy));
        })
        .AddRazorPagesOptions(options =>
        {
            options.Conventions.AllowAnonymousToFolder("/Account");
        });
    }
    

    我不完全了解不同的身份验证是如何链接或相互依赖的。我明白,OpenId在内部使用某种JWT,仍然存在以下问题:

    • 为什么Web Api示例只使用JWT,而另一个使用OpenId和Cookie?
    • 为什么OpenId示例首先不使用JWT?
    • 自定义JWT是否适用于OpenId?
    • 是否可以引入自定义标识存储,但保留Azure AD以进行登录(以及登录名称)?

    如果你能给我一些指导,没有完全可行的例子,那就太好了(尽管这会很棒)

1 个答案:

答案 0 :(得分:2)

如果您想在ASP.NET Core Web应用程序中组合Cookie和Bearer授权,可以按照下面的代码段进行操作:

services.AddAuthentication(sharedOptions =>
{
    sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    sharedOptions.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddJwtBearer(jwtOptions=> {
    jwtOptions.IncludeErrorDetails = true;
    jwtOptions.Authority = "{Authority}";
    jwtOptions.Audience = "{Audience}";
    jwtOptions.TokenValidationParameters = new TokenValidationParameters()
    {
        ValidIssuer = "{ValidIssuer}",
        ValidAudience = "{ValidAudience}"
    };
    jwtOptions.Events = new JwtBearerEvents()
    {
        OnAuthenticationFailed = context => {
            //TODO:
            return Task.FromResult(0);
        },
        OnTokenValidated = context => {
            //At this point, the security token has been validated successfully and a ClaimsIdentity has been created
            var claimsIdentity = (ClaimsIdentity)context.Principal.Identity;
            //add your custom claims here
            claimsIdentity.AddClaim(new Claim("test", "helloworld!!!"));

            return Task.FromResult(0);
        }
    };
})
.AddAzureAd(options => Configuration.Bind("AzureAd", options))
.AddCookie();

我注意到您添加了全局AuthorizeFilter,此时您需要确保匿名操作需要使用AllowAnonymous属性进行修饰。

services.AddMvc(options =>
{
    var policy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .AddAuthenticationSchemes(CookieAuthenticationDefaults.AuthenticationScheme, JwtBearerDefaults.AuthenticationScheme)
        .Build();
    options.Filters.Add(new AuthorizeFilter(policy));
})

或者您可以使用Authorize属性修饰控制器操作,如下所示:

[Authorize(AuthenticationSchemes = "Cookies,Bearer")]
public IActionResult UserInfo()
{
    return Json(User.Claims.Select(c => new { key = c.Type, value = c.Value }));
}

对于OpenID Connect中间件,您可以修改Configure(string name, OpenIdConnectOptions options)文件下的AzureAdAuthenticationBuilderExtensions.cs方法,以添加您的自定义声明(例如角色等),如下所示:

public void Configure(string name, OpenIdConnectOptions options)
{
    options.ClientId = _azureOptions.ClientId;
    options.Authority = $"{_azureOptions.Instance}{_azureOptions.TenantId}";
    options.UseTokenLifetime = true;
    options.CallbackPath = _azureOptions.CallbackPath;
    options.RequireHttpsMetadata = false;
    //the new code
    options.Events = new OpenIdConnectEvents
    {
        OnTokenValidated = context =>
        {   
            var claimsIdentity = (ClaimsIdentity)context.Principal.Identity;
            //add your custom claims here
            claimsIdentity.AddClaim(new Claim("test", "helloworld!!!"));

            return Task.FromResult(0);
        }
    };
}

总之,在配置时,您可以组合Cookie和Bearer身份验证,并且在验证令牌后,您可以检索用户标识符并从数据库中获取其他用户信息,然后将其附加到ClaimsIdentity

对于桌面客户端,您可以使用ADAL(适用于AD app v1.0)或MSAL(适用于AD app v2.0)登录并检索access_token或{ {1}},然后将其用作持票人令牌来访问您的Web应用程序。