Asp Core:Azure Ad Auth + 自定义 JWT + 自定义身份存储

Posted

技术标签:

【中文标题】Asp Core:Azure Ad Auth + 自定义 JWT + 自定义身份存储【英文标题】:Asp Core: Azure Ad Auth + custom JWT + custom Identity store 【发布时间】:2018-09-15 01:29:16 【问题描述】:

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

    通过 Azure AD(注册应用)进行身份验证 自定义 JWT 作为身份验证方案 使 Web 应用程序身份验证跨服务器/实例工作 能够保存承载以使用桌面客户​​端登录 拥有一个自定义身份存储来引入自定义角色、策略和其他内容。

所有这些部分都有工作示例,但在尝试将它们结合起来时,我偶然发现了一些问题。

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
        );
    

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

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】:

如果您想在您的 ASP.NET Core Web 应用程序中结合使用 Cookies 和 Bearer 授权,您可以按照下面的代码 sn-p:

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 中间件,您可以修改 AzureAdAuthenticationBuilderExtensions.cs 文件下的 Configure(string name, OpenIdConnectOptions options) 方法以添加您的自定义声明(例如角色等),如下所示:

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);
        
    ;

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

对于桌面客户端,您可以利用ADAL(适用于 AD 应用 v1.0)或MSAL(适用于 AD 应用 v2.0)登录并检索 access_tokenid_token,然后使用它作为不记名令牌访问您的 Web 应用程序。

【讨论】:

但我仍然想提一下,如果您在 OpenId 验证后添加自定义声明并基于这些声明构建您的令牌,您将在 JWTBearer 验证后再次添加它们。但你肯定是为了一个完整的例子这样做的。

以上是关于Asp Core:Azure Ad Auth + 自定义 JWT + 自定义身份存储的主要内容,如果未能解决你的问题,请参考以下文章

Azure AD 与 ASP.NET Core MVC 中的标识

Azure AD 限制整个 ASP.NET Core API

ASP.NET Core 2.2 中的 Azure AD 身份验证

ASP.NET Core 3.1 Azure AD 身份验证引发 OptionsValidationException

如何在 ASP.NET Core 3 上同时使用 Azure AD 身份验证和身份?

使用 Azure AD 的 Asp.net core mvc 基于角色的授权