【问题标题】:Asp Core: Azure Ad Auth + custom JWT + custom Identity storeAsp Core:Azure Ad Auth + 自定义 JWT + 自定义身份存储
【发布时间】:2023-03-24 17:58:01
【问题描述】:

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

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

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

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 用于登录(和登录名)?

如果您能给我一些指导,那就太好了,不需要完整的工作示例(即使这很好)

【问题讨论】:

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


    【解决方案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 验证后再次添加它们。但你肯定是为了一个完整的例子这样做的。
    猜你喜欢
    • 2020-07-29
    • 1970-01-01
    • 1970-01-01
    • 2019-11-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-04-04
    • 2020-06-26
    相关资源
    最近更新 更多